Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
dfc3f76
feat(layout-v2): single-panel Linear-style sidebar
tsahimatsliah Jun 13, 2026
7becaa1
fix(layout-v2): constrain sidebar panel height so the nav scrolls
tsahimatsliah Jun 13, 2026
e9ee196
fix(layout-v2): blend sidebar into the V2 surface, Linear-style collapse
tsahimatsliah Jun 13, 2026
35f03aa
feat(layout-v2): drag-to-collapse sidebar + header expand control
tsahimatsliah Jun 13, 2026
8fe73cc
feat(layout-v2): resize sidebar and feed together, persist width
tsahimatsliah Jun 13, 2026
046fb84
feat(layout-v2): compact sidebar, top search icon, profile stats, set…
tsahimatsliah Jun 13, 2026
b5212a6
feat(layout-v2): profile-first header, footer brand + help, theme in …
tsahimatsliah Jun 13, 2026
b69fdee
feat(layout-v2): reorder primary nav + discoverable resize grip
tsahimatsliah Jun 13, 2026
64d0bbc
feat(layout-v2): simplify profile switcher to avatar + name
tsahimatsliah Jun 13, 2026
df8db9c
fix(layout-v2): match Linear's denser sidebar rows + rectangular resi…
tsahimatsliah Jun 13, 2026
4340adc
feat(layout-v2): sidebar toggle shortcut + resize/expand tooltips
tsahimatsliah Jun 13, 2026
2ad9a54
feat(layout-v2): remove Recent, restyle footer bar, fix streak popover
tsahimatsliah Jun 13, 2026
e1e1735
feat(layout-v2): add Compose icon and use it for New post
tsahimatsliah Jun 13, 2026
49b956f
feat(layout-v2): smaller, hover-only section collapse chevron
tsahimatsliah Jun 13, 2026
25f79cb
fix(layout-v2): shrink section chevron and make it truly hover-only
tsahimatsliah Jun 13, 2026
c86be28
fix(layout-v2): align squad avatars with other icons + wider icon gap
tsahimatsliah Jun 13, 2026
0e8cacd
feat(layout-v2): polish footer strip + change collapse shortcut to "["
tsahimatsliah Jun 13, 2026
8408323
feat(layout-v2): smaller profile name + search field under the header
tsahimatsliah Jun 13, 2026
298a2b3
feat(layout-v2): settings takeover swaps header for a Back control
tsahimatsliah Jun 13, 2026
c531edf
feat(layout-v2): use Primary variant for the New post button
tsahimatsliah Jun 13, 2026
118f2d3
feat(layout-v2): tweak header — streak count left, smaller New post +…
tsahimatsliah Jun 13, 2026
eb7d26b
feat(layout-v2): order Feeds above Saved in the sidebar
tsahimatsliah Jun 13, 2026
79e8fe5
feat(layout-v2): collapsed sidebar peeks as an overlay on hover
tsahimatsliah Jun 13, 2026
3c3cdf8
feat(layout-v2): move search into the scrollable list (not sticky)
tsahimatsliah Jun 14, 2026
541ed2c
feat(layout-v2): soft right border on the hover-peek overlay
tsahimatsliah Jun 14, 2026
6deaac1
feat(layout-v2): always-visible resize divider with hover indicator
tsahimatsliah Jun 14, 2026
d511aa5
feat(layout-v2): always-visible centered resize grip (no full-height …
tsahimatsliah Jun 14, 2026
da20274
feat(layout-v2): resize grip — rounded rect, light→blue, full-height …
tsahimatsliah Jun 14, 2026
ca451b1
feat(layout-v2): thinner resize grip that widens on hover
tsahimatsliah Jun 14, 2026
ca52ebd
feat(layout-v2): add Help icon and use it for the support button
tsahimatsliah Jun 15, 2026
b548f5e
chore: re-trigger preview deploy
tsahimatsliah Jun 15, 2026
5a6e695
feat(layout-v2): collapsed sidebar is now an icon rail (production-st…
tsahimatsliah Jun 16, 2026
a1a9f41
feat(layout-v2): no hover-open, centered rail icons, grip-to-open on …
tsahimatsliah Jun 16, 2026
ff67c37
Merge branch 'main' into claude/objective-blackburn-47bee7
tsahimatsliah Jun 16, 2026
e89039b
feat(layout-v2): narrower centered icon rail + drag-to-open grip
tsahimatsliah Jun 16, 2026
d72e56f
Merge branch 'main' into claude/objective-blackburn-47bee7
tsahimatsliah Jun 16, 2026
dab8d49
fix(layout-v2): center rail icons, easier drag-open, icon-only streak
tsahimatsliah Jun 16, 2026
e45a0d5
fix(layout-v2): reliable drag-to-open with preview + release hint
tsahimatsliah Jun 16, 2026
af51aea
refactor(layout-v2): remove feed-header "open sidebar" button
tsahimatsliah Jun 16, 2026
9be036f
feat(layout-v2): rail streak shows small flame + count
tsahimatsliah Jun 16, 2026
c1e9d90
fix(layout-v2): stronger contrast on the "Release to open" hint
tsahimatsliah Jun 16, 2026
9738e10
Merge branch 'main' into claude/objective-blackburn-47bee7
tsahimatsliah Jun 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions packages/shared/src/components/MainFeedLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ import { useReadingReminderHero } from '../hooks/notifications/useReadingReminde
import { useTrackQuestClientEvent } from '../hooks/useTrackQuestClientEvent';
import { useReadingReminderVariation } from '../hooks/notifications/useReadingReminderVariation';
import { useLayoutVariant } from '../hooks/layout/useLayoutVariant';
import { ExploreSectionTabs } from './header/ExploreSectionTabs';
import { ExploreSortDropdown } from './header/ExploreSortDropdown';

const FeedExploreHeader = dynamic(
() =>
Expand Down Expand Up @@ -263,6 +265,7 @@ export default function MainFeedLayout({
isPopular,
isAnyExplore,
isExploreLatest,
isDiscussed,
isSortableFeed,
isCustomFeed,
isSearch: isSearchPage,
Expand Down Expand Up @@ -726,7 +729,10 @@ export default function MainFeedLayout({
// page-header strip (matching the SquadDirectoryLayout pattern). The
// inline FeedExploreComponent is suppressed below to avoid showing
// the same tabs twice.
const showExploreV2PageHeader = isAnyExplore && isV2;
// The Discussions feed (/discussed) is part of the Explore hub — show the
// same section tabs there so the hub persists. The Sort dropdown is only
// for the actual Explore sorts, so it stays gated on isAnyExplore.
const showExploreV2PageHeader = (isAnyExplore || isDiscussed) && isV2;

// v2 also hoists the regular page-header strip up here, OUTSIDE
// `FeedPageLayoutComponent`, so it can span the full floating-card
Expand Down Expand Up @@ -765,13 +771,8 @@ export default function MainFeedLayout({
<>
{showExploreV2PageHeader && (
<header className={classNames(pageHeaderClassName, '!py-0')}>
<FeedExploreHeader
directoryTabs
tab={tab}
setTab={onTabChange}
showBreadcrumbs={false}
className={{ container: 'min-w-0 flex-1' }}
/>
<ExploreSectionTabs />
{isAnyExplore && <ExploreSortDropdown />}
</header>
)}
{showFeedV2PageHeader && (
Expand Down
10 changes: 8 additions & 2 deletions packages/shared/src/components/MainLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -301,19 +301,25 @@ function MainLayoutComponent({
/>
)}
<main
data-resizable-pane
className={classNames(
'flex flex-col',
animateContentPadding &&
'transition-[padding] duration-300 ease-in-out',
!sidebarOwnsHeader && 'laptop:pt-16',
showSidebar &&
(isV2 ? 'tablet:pl-16 laptop:pl-16' : 'tablet:pl-16 laptop:pl-11'),
// v2 collapses to a narrow icon rail (laptop:pl-14); expanded, the
// padding tracks the resizable panel width below. Tablet keeps the
// icon SidebarTablet width.
(isV2 ? 'tablet:pl-16 laptop:pl-14' : 'tablet:pl-16 laptop:pl-11'),
className,
isAuthReady &&
showSidebar &&
(sidebarExpanded || forceSidebarExpanded) &&
(isV2
? 'laptop:!pl-[19rem]'
? // Matches the resizable sidebar width (see SidebarDesktopV2);
// the var is updated live while dragging so content tracks it.
'laptop:!pl-[var(--sidebar-width,19rem)]'
: !isScreenCentered && 'laptop:!pl-60'),
isBannerAvailable && !sidebarOwnsHeader && 'laptop:pt-24',
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,14 @@ export const ProfileMenuHeader = ({
className,
shouldOpenProfile = false,
profileImageSize = ProfileImageSize.Large,
}: Props): ReactElement => {
}: Props): ReactElement | null => {
const { user } = useAuthContext();
const { isPlus } = usePlusSubscription();

if (!user) {
return null;
}

return (
<ConditionalWrapper
condition={shouldOpenProfile}
Expand All @@ -51,7 +55,7 @@ export const ProfileMenuHeader = ({
className="!rounded-10 border-background-default"
/>

<div className="flex min-w-0 flex-1 flex-col gap-1">
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<div className="flex items-center gap-1">
<Typography
type={TypographyType.Subhead}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export const ProfileSectionItem = ({
color={typography?.color ?? TypographyColor.Tertiary}
type={typography?.type ?? TypographyType.Subhead}
className={classNames(
'flex h-10 cursor-pointer items-center gap-2 rounded-10 px-1 tablet:h-8',
'group flex h-10 cursor-pointer items-center gap-2 rounded-10 px-1 tablet:h-8',
(href || onClick) && 'hover:bg-surface-float',
isActive ? 'bg-surface-active' : undefined,
className,
Expand All @@ -81,7 +81,7 @@ export const ProfileSectionItem = ({

{!isMobile && showLinkIcon && (
<OpenLinkIcon
className="ml-auto text-text-quaternary"
className="ml-auto text-text-quaternary opacity-0 transition-opacity group-focus-within:opacity-100 group-hover:opacity-100"
size={IconSize.Size16}
/>
)}
Expand Down
12 changes: 7 additions & 5 deletions packages/shared/src/components/fields/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,14 @@ export function Dropdown({
size={buttonSize}
disabled={disabled}
className={classNames(
// `!pl-4 !pr-2.5` overrides the Button's built-in Large padding (px-6)
// so the value lines up with the other fields' 16px text inset and the
// chevron sits tight to the right edge instead of floating 24px in.
'group flex w-full items-center !pl-4 !pr-2.5 font-normal text-text-secondary typo-body hover:bg-surface-hover hover:text-text-primary',
'group flex items-center font-normal text-text-secondary typo-body hover:bg-surface-hover hover:text-text-primary',
// Value dropdowns get `!pl-4 !pr-2.5` so the value lines up with other
// fields' 16px text inset and the chevron sits tight to the right edge.
// Icon-only dropdowns drop that and render as a square instead.
iconOnly
? 'aspect-square justify-center !px-0'
: 'w-full !pl-4 !pr-2.5',
className?.button,
iconOnly && 'items-center justify-center',
)}
onClick={fullScreen ? handleMenuTrigger : undefined}
onKeyDown={handleKeyboard}
Expand Down
20 changes: 20 additions & 0 deletions packages/shared/src/components/header/ExploreHubHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { ReactElement, ReactNode } from 'react';
import React from 'react';
import { PageHeader } from '../layout/PageHeader';
import { ExploreSectionTabs } from './ExploreSectionTabs';

// Shared v2 header for the Explore hub's directory pages (Tags, Sources,
// Leaderboard, Best of). Keeps the section-tab strip and its height
// (`!py-0`) consistent in one place. Optional children render as header
// actions (e.g. the "Suggest source" button).
export function ExploreHubHeader({
children,
}: {
children?: ReactNode;
}): ReactElement {
return (
<PageHeader title={<ExploreSectionTabs />} className="!py-0">
{children}
</PageHeader>
);
}
53 changes: 53 additions & 0 deletions packages/shared/src/components/header/ExploreSectionTabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { ReactElement } from 'react';
import React from 'react';
import { useRouter } from 'next/router';
import {
SquadDirectoryNavbar,
SquadDirectoryNavbarItem,
} from '../squads/layout/SquadDirectoryNavbar';
import { ButtonSize } from '../buttons/Button';

type ExploreSection = {
label: string;
path: string;
// The tab is active when the current path equals `match` or sits under it
// (e.g. /tags/react keeps the Tags tab active).
match: string;
};

const sections: ExploreSection[] = [
{ label: 'Explore', path: '/posts', match: '/posts' },
{ label: 'Tags', path: '/tags', match: '/tags' },
{ label: 'Sources', path: '/sources', match: '/sources' },
{ label: 'Leaderboard', path: '/users', match: '/users' },
{ label: 'Discussions', path: '/discussed', match: '/discussed' },
];

// Primary navbar for the unified Explore hub (v2). Sits above the Explore
// feed's sort tabs and on the Tags/Sources/Leaderboard/Discussions pages so
// the sections stay one click apart after Discover was folded into Home.
export function ExploreSectionTabs(): ReactElement {
const router = useRouter();
const currentPath = (router.asPath || router.pathname).split('?')[0];

return (
<SquadDirectoryNavbar
aria-label="Explore sections"
className="!mx-0 min-w-0 flex-1 !border-0 !px-0"
>
{sections.map((section) => (
<SquadDirectoryNavbarItem
key={section.label}
buttonSize={ButtonSize.Small}
isActive={
currentPath === section.match ||
currentPath.startsWith(`${section.match}/`)
}
label={section.label}
ariaLabel={`Show ${section.label}`}
path={section.path}
/>
))}
</SquadDirectoryNavbar>
);
}
62 changes: 62 additions & 0 deletions packages/shared/src/components/header/ExploreSortDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { ReactElement } from 'react';
import React from 'react';
import { useRouter } from 'next/router';
import { Dropdown } from '../fields/Dropdown';
import { CalendarIcon } from '../icons';
import { IconSize } from '../Icon';
import { ButtonSize, ButtonVariant } from '../buttons/Button';
import { ExploreTabs, tabToUrl, urlToTab } from './FeedExploreHeader';
import { QueryStateKeys, useQueryState } from '../../hooks/utils/useQueryState';
import { periodTexts } from '../layout/common';

const sortLabels = Object.values(ExploreTabs);
const sortsWithPeriod: ExploreTabs[] = [
ExploreTabs.MostUpvoted,
ExploreTabs.BestDiscussions,
];

// v2 Explore: switch the feed's ranking via a "Sort" dropdown rather than a
// second row of tabs — sorting a feed isn't navigating to a sibling page, so
// a dropdown reads cleaner (Reddit/GitHub pattern). Each sort is still its
// own route, so selecting one navigates.
export function ExploreSortDropdown(): ReactElement {
const router = useRouter();
const currentPath = (router.asPath || router.pathname).split('?')[0];
const activeTab = urlToTab[currentPath] ?? ExploreTabs.Popular;
const selectedIndex = Math.max(0, sortLabels.indexOf(activeTab));
const [period, setPeriod] = useQueryState({
key: [QueryStateKeys.FeedPeriod],
defaultValue: 0,
});

return (
<span className="ml-auto flex items-center gap-2">
{sortsWithPeriod.includes(activeTab) && (
<Dropdown
iconOnly
shouldIndicateSelected
icon={<CalendarIcon size={IconSize.Small} />}
buttonSize={ButtonSize.Small}
buttonVariant={ButtonVariant.Float}
selectedIndex={period}
options={periodTexts}
onChange={(_, index) => setPeriod(index)}
buttonAriaLabel="Filter by date range"
/>
)}
<Dropdown
selectedIndex={selectedIndex}
options={sortLabels}
buttonSize={ButtonSize.Small}
buttonVariant={ButtonVariant.Float}
buttonAriaLabel="Sort posts"
onChange={(value) => {
const url = tabToUrl[value as ExploreTabs];
if (url) {
router.push(url).catch(() => undefined);
}
}}
/>
</span>
);
}
11 changes: 4 additions & 7 deletions packages/shared/src/components/icons/Calendar/filled.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 10 additions & 7 deletions packages/shared/src/components/icons/Calendar/outlined.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions packages/shared/src/components/icons/Compose/filled.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions packages/shared/src/components/icons/Compose/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { ReactElement } from 'react';
import React from 'react';
import type { IconProps } from '../../Icon';
import Icon from '../../Icon';
import OutlinedIcon from './outlined.svg';
import FilledIcon from './filled.svg';

export const ComposeIcon = (props: IconProps): ReactElement => (
<Icon {...props} IconPrimary={OutlinedIcon} IconSecondary={FilledIcon} />
);
Loading
Loading