Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
3cdbd3f
Start screen: intent tabs on the composer, profile and RFP as chips
yattias Sep 22, 2026
b4eed0e
My Funding: totals in the rail, workspace composer under the tabs
yattias Sep 22, 2026
3b946d4
Enter the workspace from Publish and My Funding, not the nav
yattias Sep 22, 2026
e6588d3
Workspace strip names what is open; funders get a concierge card
yattias Sep 22, 2026
58579a0
Draft rows carry their status; the journey timeline connects
yattias Sep 23, 2026
6d508a2
My Funding lists RFPs and proposals as one kind of row
yattias Sep 23, 2026
98867e5
My Funding rows: live work first, with its image and its money
yattias Sep 23, 2026
d95be8e
Focus the composer on a new conversation; one form input per field
yattias Sep 23, 2026
bbccf98
RFP guidelines, workspace steps aside on publish, deletable drafts
yattias Sep 23, 2026
b373b85
Draft rows: delete lives behind a three-dots menu
yattias Sep 23, 2026
6c7944c
Start screen: words-only timeline, privacy line under the composer
yattias Sep 23, 2026
477c6aa
Start screen cards, "Let's get started", RFP hero tabs beside the pool
yattias Sep 23, 2026
a0188b8
RFP hero footnote breathes; pool contribution says what it does
yattias Sep 23, 2026
0c33f04
Pool modal: the note sits under the amount, with coins; no raised line
yattias Sep 23, 2026
eb4f54c
Revert the Publish menu and homepage tab icons to the originals
yattias Sep 23, 2026
751f2ea
Document pane: the drafting row says only "Drafting"
yattias Sep 23, 2026
156de8c
Workspace: the start screen is "New RFP" or "New proposal"
yattias Sep 23, 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
32 changes: 5 additions & 27 deletions app/layouts/MobileBottomNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
faBars,
} from '@fortawesome/pro-light-svg-icons';
import { faXTwitter, faDiscord, faGithub, faLinkedin } from '@fortawesome/free-brands-svg-icons';
import { BriefcaseBusiness, Sprout, Star } from 'lucide-react';
import { Sprout, Star } from 'lucide-react';
import { ChangelogLink } from '@/components/changelog/ChangelogLink';
import { FundingPowerBar } from '@/components/Funding/FundingPowerBar';
import { Icon } from '@/components/ui/icons';
Expand All @@ -24,10 +24,6 @@ import { useAuthenticatedAction } from '@/contexts/AuthModalContext';
import { useCurrencyPreference } from '@/contexts/CurrencyPreferenceContext';
import { useScrollContainer } from '@/contexts/ScrollContainerContext';
import { isHomeTabPath } from '@/hooks/useFundTabs';
import { useUser } from '@/contexts/UserContext';
import { useOptionalAIMode } from '@/components/AIMode/AIModeContext';
import { AI_MODE_NAME } from '@/components/AIMode/copy';
import { isHubEditorOrModerator } from '@/utils/permissions';

interface NavItem {
label: string;
Expand All @@ -36,8 +32,6 @@ interface NavItem {
isMore?: boolean;
requiresAuth?: boolean;
isHome?: boolean;
/** Opens the workspace in place, on its new-conversation screen, instead of navigating. */
isAIMode?: boolean;
}

// Additional navigation items not in the bottom bar
Expand Down Expand Up @@ -76,17 +70,11 @@ export const MobileBottomNav: React.FC = () => {
const { executeAuthenticatedAction } = useAuthenticatedAction();
const { showUSD, toggleCurrency } = useCurrencyPreference();
const scrollContainerRef = useScrollContainer();
const { user } = useUser();
const aiMode = useOptionalAIMode();

// Moderators and hub editors, the only users the workspace admits, get it
// in the bar where Peer Review sits for everyone else.
const mainNavItems: NavItem[] = [
{ label: 'Home', href: '/', iconKey: 'home', isHome: true },
{ label: 'My Funding', href: '/my-funding', iconKey: 'fund', requiresAuth: true },
isHubEditorOrModerator(user)
? { label: AI_MODE_NAME, iconKey: 'workspace', isAIMode: true }
: { label: 'Peer Review', href: '/peer-review', iconKey: 'peer-review' },
{ label: 'Peer Review', href: '/peer-review', iconKey: 'peer-review' },
{ label: 'Wallet', href: '/researchcoin', iconKey: 'wallet' },
{ label: 'More', isMore: true, iconKey: 'more' },
];
Expand Down Expand Up @@ -117,10 +105,6 @@ export const MobileBottomNav: React.FC = () => {
setIsMoreOpen(true);
return;
}
if (item.isAIMode) {
aiMode?.selectChat(null);
return;
}

if (item.requiresAuth) {
executeAuthenticatedAction(() => router.push(item.href!));
Expand Down Expand Up @@ -151,10 +135,6 @@ export const MobileBottomNav: React.FC = () => {
color={iconColor}
/>
);
case 'workspace':
return (
<BriefcaseBusiness size={iconSize} color={iconColor} strokeWidth={isActive ? 2.25 : 2} />
);
case 'peer-review':
return (
<Star
Expand Down Expand Up @@ -248,11 +228,9 @@ export const MobileBottomNav: React.FC = () => {
{mainNavItems.map((item) => {
const isActive = item.isMore
? isMoreActive || isMoreOpen
: item.isAIMode
? Boolean(aiMode?.isOpen)
: item.href
? isPathActive(item.href, pathname, item.isHome)
: false;
: item.href
? isPathActive(item.href, pathname, item.isHome)
: false;

return (
<button
Expand Down
56 changes: 7 additions & 49 deletions app/layouts/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,8 @@ import { IconName } from '@/components/ui/icons/Icon';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faHouse as faHouseSolid } from '@fortawesome/pro-solid-svg-icons';
import { faHouse as faHouseLight } from '@fortawesome/pro-light-svg-icons';
import { BriefcaseBusiness, Sprout, Star } from 'lucide-react';
import { useOptionalAIMode } from '@/components/AIMode/AIModeContext';
import { AI_MODE_NAME } from '@/components/AIMode/copy';
import { Sprout, Star } from 'lucide-react';
import { isHomeTabPath } from '@/hooks/useFundTabs';
import { useUser } from '@/contexts/UserContext';
import { isHubEditorOrModerator } from '@/utils/permissions';
import { cn } from '@/utils/styles';

interface NavIcon {
Expand All @@ -34,8 +30,6 @@ interface NavigationItem {
isLucideStar?: boolean;
isLucideSprout?: boolean;
isHome?: boolean;
/** Toggles the AI Mode overlay in place instead of navigating. */
isAIMode?: boolean;
}

interface NavigationProps {
Expand Down Expand Up @@ -64,10 +58,6 @@ export const Navigation: React.FC<NavigationProps> = ({
onUnimplementedFeature,
forceMinimize = false,
}) => {
const { user } = useUser();
// The assistant is gated server-side to moderators and hub editors; nobody
// else gets a door to a room they cannot enter.
const canUseAssistant = isHubEditorOrModerator(user);
const navigationItems: NavigationItem[] = [
{
label: 'Home',
Expand Down Expand Up @@ -102,13 +92,6 @@ export const Navigation: React.FC<NavigationProps> = ({
isLucideSprout: true,
description: 'Learn about the ResearchHub Endowment',
},
{
label: AI_MODE_NAME,
href: '#',
isAIMode: true,
requiresAuth: true,
description: 'Draft proposals and RFPs with the assistant',
},
];

const getButtonStyles = (isActive: boolean) => {
Expand Down Expand Up @@ -149,8 +132,7 @@ export const Navigation: React.FC<NavigationProps> = ({
}> = ({ item, onUnimplementedFeature }) => {
const { executeAuthenticatedAction } = useAuthenticatedAction();
const router = useRouter();
const aiMode = useOptionalAIMode();
const isActive = item.isAIMode ? Boolean(aiMode?.isOpen) : isPathActive(item.href, item.isHome);
const isActive = isPathActive(item.href, item.isHome);
const buttonStyles = getButtonStyles(isActive);

const iconColor = isActive ? '#3971ff' : '#404040';
Expand Down Expand Up @@ -187,27 +169,7 @@ export const Navigation: React.FC<NavigationProps> = ({
? 'hidden'
: 'flex w-full min-w-0 items-center tablet:max-sidebar-compact:!hidden';

if (item.isAIMode) {
// Same row as the links, but no navigation: the workspace opens in
// place on its new-conversation screen and the URL only gains a query
// param.
return (
<button
type="button"
onClick={() => executeAuthenticatedAction(() => aiMode?.selectChat(null))}
className={buttonStyles}
aria-pressed={isActive}
title={item.description}
>
<div className={iconContainerClass}>
<BriefcaseBusiness size={22} color={iconColor} strokeWidth={isActive ? 2.25 : 2} />
</div>
<div className={textContainerClass}>
<span className="inline-flex min-w-0 items-center gap-2 truncate">{item.label}</span>
</div>
</button>
);
}
const label = <span className="min-w-0 truncate">{item.label}</span>;

return (
<Link
Expand Down Expand Up @@ -239,9 +201,7 @@ export const Navigation: React.FC<NavigationProps> = ({
<div className="w-[26px] h-[26px]" />
)}
</div>
<div className={textContainerClass}>
<span className="inline-flex min-w-0 items-center gap-2 truncate">{item.label}</span>
</div>
<div className={textContainerClass}>{label}</div>
</Link>
);
};
Expand All @@ -255,11 +215,9 @@ export const Navigation: React.FC<NavigationProps> = ({
)}
>
<div className="space-y-2">
{navigationItems
.filter((item) => !item.isAIMode || canUseAssistant)
.map((item) => (
<NavLink key={item.label} item={item} onUnimplementedFeature={onUnimplementedFeature} />
))}
{navigationItems.map((item) => (
<NavLink key={item.label} item={item} onUnimplementedFeature={onUnimplementedFeature} />
))}
</div>
</nav>
);
Expand Down
7 changes: 7 additions & 0 deletions app/layouts/PageLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ interface PageLayoutProps {
sidebarContentClassName?: string;
topBanner?: ReactNode;
rightSidebarAbove?: ReactNode;
/**
* With a card above it the gray rail normally sizes to its content; this
* keeps it stretched to the bottom of the column regardless.
*/
rightSidebarFill?: boolean;
fundraiseGrantId?: number;
/**
* `default` centers the page in a 1180px container and lets the main column
Expand All @@ -54,6 +59,7 @@ function PageLayoutInner({
sidebarContentClassName,
topBanner,
rightSidebarAbove,
rightSidebarFill = false,
contentWidth = 'default',
wideRow = false,
}: PageLayoutProps) {
Expand Down Expand Up @@ -144,6 +150,7 @@ function PageLayoutInner({
rightSidebar={rightSidebar}
contentClassName={sidebarContentClassName}
aboveSidebar={rightSidebarAbove}
fill={rightSidebarFill}
/>
)}
</div>
Expand Down
52 changes: 32 additions & 20 deletions app/layouts/PublishMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,28 @@ import {
OpenProposalModal,
type ProposalCreationMethod,
} from '@/components/Funding/OpenProposalModal';
import type { FundingIntent } from '@/components/Funding/fundingDirection';
import { useFundingDrafting } from '@/components/Funding/useFundingDrafting';
import { useScreenSize } from '@/hooks/useScreenSize';
import { useState } from 'react';

interface PublishMenuProps {
forceMinimize?: boolean;
}

const PUBLISH_MENU_SECTIONS = [
interface PublishMenuItem {
readonly id: string;
readonly title: string;
readonly description: string;
readonly icon: React.ReactNode;
/** Which side of the money the item starts: an RFP funds, a proposal needs funding. */
readonly intent: FundingIntent;
}

const PUBLISH_MENU_SECTIONS: readonly {
readonly title: string;
readonly items: readonly PublishMenuItem[];
}[] = [
{
title: 'Publish on ResearchHub',
items: [
Expand All @@ -31,7 +45,7 @@ const PUBLISH_MENU_SECTIONS = [
title: 'Request for Proposal',
description: 'Fund specific research you care about',
icon: <FontAwesomeIcon icon={faBullhorn} className="h-[18px] w-[18px] text-gray-700" />,
handler: 'handleOpenGrant',
intent: 'fund',
},
{
id: 'request-funding',
Expand All @@ -40,11 +54,11 @@ const PUBLISH_MENU_SECTIONS = [
icon: (
<FontAwesomeIcon icon={faFileSignature} className="h-[18px] w-[18px] text-gray-700" />
),
handler: 'handleFundResearch',
intent: 'need_funding',
},
],
},
] as const;
];

interface MenuItemContentProps {
icon: React.ReactNode;
Expand All @@ -69,41 +83,39 @@ const MenuItemContent: React.FC<MenuItemContentProps> = ({ icon, title, descript
);
};

/**
* The sidebar's Publish button: a Request for Proposal or a Proposal. For a
* user the workspace admits, either opens it straight onto a conversation
* for that side of the money; everyone else gets the notebook's opening
* modal and editor, as before.
*/
export const PublishMenu: React.FC<PublishMenuProps> = ({ forceMinimize = false }) => {
const router = useRouter();
const { executeAuthenticatedAction } = useAuthenticatedAction();
const { inWorkspace, startNew } = useFundingDrafting();
const { smAndDown } = useScreenSize();
const [isMobileDrawerOpen, setIsMobileDrawerOpen] = useState(false);
const [isFundingOpportunityModalOpen, setIsFundingOpportunityModalOpen] = useState(false);
const [isProposalModalOpen, setIsProposalModalOpen] = useState(false);

const handleFundResearch = () => {
setIsProposalModalOpen(true);
};

const handleConfirmCreateProposal = (method: ProposalCreationMethod) => {
setIsProposalModalOpen(false);
router.push(`/notebook?newFunding=true&proposalSource=${method}`);
};

const handleOpenGrant = () => {
setIsFundingOpportunityModalOpen(true);
};

const handleConfirmOpenGrant = (method: FundingOpportunityCreationMethod) => {
setIsFundingOpportunityModalOpen(false);
router.push(`/notebook?newGrant=true&grantSource=${method}`);
};

const handleMenuItemClick = (item: (typeof PUBLISH_MENU_SECTIONS)[number]['items'][number]) => {
const handleMenuItemClick = (item: PublishMenuItem) => {
executeAuthenticatedAction(() => {
switch (item.handler) {
case 'handleFundResearch':
handleFundResearch();
break;
case 'handleOpenGrant':
handleOpenGrant();
break;
if (inWorkspace) {
startNew(item.intent);
} else if (item.intent === 'fund') {
setIsFundingOpportunityModalOpen(true);
} else {
setIsProposalModalOpen(true);
}
});

Expand Down
6 changes: 5 additions & 1 deletion app/layouts/components/RightSidebarContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,15 @@ interface RightSidebarContainerProps {
rightSidebar: boolean | ReactNode;
contentClassName?: string;
aboveSidebar?: ReactNode;
/** Stretch the gray rail to the bottom of the column even with a card above it. */
fill?: boolean;
}

export function RightSidebarContainer({
rightSidebar,
contentClassName,
aboveSidebar,
fill = false,
}: RightSidebarContainerProps) {
const { mobileSidebarOpen, setMobileSidebarOpen } = useWorkTab();
const isDefaultSidebar = typeof rightSidebar === 'boolean';
Expand Down Expand Up @@ -60,7 +63,8 @@ export function RightSidebarContainer({
// of filling the column — otherwise a short or empty sidebar leaves
// a tall empty rail hanging beneath that card. It still shrinks and
// scrolls when the content is taller than the space available.
cardAboveSidebar ? 'flex-initial' : 'flex-1 h-full'
// A page can ask for the full column anyway.
cardAboveSidebar && !fill ? 'flex-initial' : 'flex-1 h-full'
)}
>
<div className={cn('h-full', contentClassName)}>
Expand Down
5 changes: 0 additions & 5 deletions app/layouts/topbar/TopBarUserControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import { Button } from '@/components/ui/Button';
import UserMenu from '@/components/menus/UserMenu';
import type { User } from '@/types/user';
import { TopBarSearchButton } from './TopBarSearchButton';
import { TopBarWorkspaceButton } from './TopBarWorkspaceButton';
import { isHubEditorOrModerator } from '@/utils/permissions';
import { formatBadgeCount, formatRSC } from '@/utils/number';
import { UserService } from '@/services/user.service';
import { cn } from '@/lib/utils';
Expand Down Expand Up @@ -103,9 +101,6 @@ export const TopBarUserControls = ({
<div className="hidden tablet:!flex items-center space-x-2 h-full">
<TopBarSearchButton onClick={onSearchOpen} />

{/* The workspace's door here is gated like its nav item. */}
{user && isHubEditorOrModerator(user) && <TopBarWorkspaceButton />}

{isLoading ? (
<>
<DesktopIconSkeleton />
Expand Down
Loading
Loading