Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
aadf4b9
feat(layout-v2): sidebar iteration fixes — logo/home combo, streak st…
tsahimatsliah Jul 29, 2026
1343b0a
fix(layout-v2): actually consume the rail drag click guard
tsahimatsliah Jul 29, 2026
c963b53
feat(layout-v2): move New post below the rail tabs
tsahimatsliah Jul 29, 2026
e9e1bcb
fix(layout-v2): use the filter icon for Feed settings in the You panel
tsahimatsliah Jul 29, 2026
370c194
style(layout-v2): add breathing room below the New post button
tsahimatsliah Jul 29, 2026
864c7da
feat(layout-v2): make New post a reorderable rail item
tsahimatsliah Jul 29, 2026
28350b9
feat(layout-v2): drop Jobs and DevCard from the You panel list
tsahimatsliah Jul 29, 2026
c5ff736
fix(layout-v2): centre New post on the rail and even out its spacing
tsahimatsliah Jul 29, 2026
213737e
fix(layout-v2): arm the rail drag click guard at drag start
tsahimatsliah Jul 29, 2026
743038b
fix(layout-v2): stop rail items squashing mid-drag; give New post tab…
tsahimatsliah Jul 29, 2026
d5dda7f
fix(layout-v2): render rail reorder synchronously on drop
tsahimatsliah Jul 29, 2026
31a9648
chore: retrigger CI (known-flaky MyFeedPage test)
tsahimatsliah Jul 29, 2026
e24083a
fix(layout-v2): make the post-drag click guard consume-based
tsahimatsliah Jul 29, 2026
36f8284
fix(layout-v2): drag rail tabs via DragOverlay so the bell's anchor n…
tsahimatsliah Jul 29, 2026
bda45d2
feat(layout-v2): move the rail drag slot skeleton live with the drag
tsahimatsliah Jul 29, 2026
e1d9b3d
style(layout-v2): dock-style chip border on the rail drag ghost
tsahimatsliah Jul 29, 2026
b156c48
style(layout-v2): match the dock exactly for rail drag ghost and skel…
tsahimatsliah Jul 29, 2026
8198d86
style(layout-v2): one shared glass drag treatment for rail and dock
tsahimatsliah Jul 30, 2026
caf1b92
style(notifications): squarer unread badge with a larger number
tsahimatsliah Jul 30, 2026
f8bcc8f
style(notifications): soften and shrink the unread badge
tsahimatsliah Jul 30, 2026
03a8802
feat(layout-v2): circular streak badge; smaller right-growing unread …
tsahimatsliah Jul 30, 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
33 changes: 26 additions & 7 deletions packages/shared/src/components/notifications/NotificationsBell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ import Link from '../utilities/Link';
import { IconSize } from '../Icon';
import { railTabClass, railTabLabelClass } from '../sidebar/common';

// Unread-count badge: a 20px rounded square (8px radius) with a bold 13px
// number — small enough not to overpower the bell glyph it notches. Shared by
// both bell variants so the header and the v2 rail stay identical.
const notificationBubbleClass =
'!min-h-5 !min-w-5 !rounded-8 !font-bold !typo-footnote tabular-nums';

function NotificationsBell({
compact,
rail,
Expand Down Expand Up @@ -70,6 +76,11 @@ function NotificationsBell({
// we only own the active text color).
role="tab"
aria-selected={atNotificationsPage}
// Every other rail tab is a <button>; this one is an anchor, which
// browsers drag natively. That native link-drag ran alongside the
// rail's dnd-kit reorder and navigated on drop — reloading
// /notifications. Only dnd-kit should drive this drag.
draggable={false}
className={classNames(
railTabClass,
atNotificationsPage && '!text-text-primary',
Expand All @@ -84,14 +95,21 @@ function NotificationsBell({
className="pointer-events-none"
/>
{hasNotification && (
// Compact corner badge: the rail bell is only `size-6` (24px),
// so the full-size shared `Bubble` (min 20px) blankets the
// glyph. A smaller pill notched into the top-right corner reads
// as a badge instead of covering the icon. `border-background-
// default` matches the sidebar surface for the cutout effect.
<span className="pointer-events-none absolute -right-2 -top-1.5 flex h-4 min-w-4 items-center justify-center rounded-full border-2 border-background-default bg-accent-cabbage-default px-1 leading-none text-white typo-caption2">
// The shared square Bubble is the design-system badge (see the
// Bell Storybook "Count bubble" reference); offset outward so
// it notches the corner without blanketing the 24px glyph.
<Bubble
className={classNames(
// Anchored by its LEFT edge (the bell box is 24px, so 14px
// puts it just past centre): multi-digit counts then grow
// rightward into the rail's padding instead of creeping
// left across the bell glyph.
'pointer-events-none !right-auto -top-2 left-[0.875rem] px-1',
notificationBubbleClass,
)}
>
{getUnreadText(unreadCount)}
</span>
</Bubble>
)}
</span>
{!railHideLabel && (
Expand Down Expand Up @@ -130,6 +148,7 @@ function NotificationsBell({
<Bubble
className={classNames(
'-right-1.5 -top-1.5 cursor-pointer px-1',
notificationBubbleClass,
compact && 'right-0 top-0',
)}
>
Expand Down
27 changes: 23 additions & 4 deletions packages/shared/src/components/quest/CompactQuestList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type { QuestReward, UserQuest } from '../../graphql/quests';
import { QuestRewardType, QuestStatus } from '../../graphql/quests';
import { useQuestDashboard } from '../../hooks/useQuestDashboard';
import { useClaimQuestReward } from '../../hooks/useClaimQuestReward';
import { getQuestDestination } from './questDestinations';

const rewardColorByType: Record<QuestRewardType, string> = {
// XP reads as plain white, normal weight (de-emphasized); Cores/Reputation
Expand Down Expand Up @@ -66,6 +67,22 @@ const CompactQuestRow = ({
const isClaimed =
quest.status === QuestStatus.Claimed || Boolean(quest.claimedAt);
const canClaim = quest.claimable && !!quest.userQuestId && !isClaimed;
// Route each quest to the surface where it can actually be completed (same
// mapping the quest dashboard uses); quests with no mapped surface — and
// claimable/claimed rows, where navigating away is not the point — fall back
// to Game Center.
const destination =
canClaim || isClaimed ? null : getQuestDestination(quest.quest);
const href = (() => {
if (destination?.path) {
return `${webappUrl}${destination.path.replace(/^\//, '')}`;
}
if (destination?.href) {
return destination.href;
}
return `${webappUrl}game-center`;
})();
const destinationLabel = destination?.label ?? 'Game Center';

return (
// Rounded hover pill + `hover:bg-surface-hover`, matching every other v2
Expand All @@ -74,13 +91,15 @@ const CompactQuestRow = ({
{/* Title row: title takes the full width with the open-details chevron in
the top-right corner. */}
<div className="flex items-start justify-between gap-1">
<Link href={`${webappUrl}game-center`} passHref>
<Link href={href} passHref>
{/* Stretched link — its `before` covers the whole row (the row is
`relative`), so clicking anywhere on the quest opens the Game
Center. The Claim button opts out via `relative z-1`; the
`relative`), so clicking anywhere on the quest goes to the quest's
own surface. The Claim button opts out via `relative z-1`; the
chevron/progress sit under the link so they navigate too. */}
<a
aria-label={`${quest.quest.name} — open Game Center`}
aria-label={`${quest.quest.name} — go to ${destinationLabel}`}
target={destination?.openInNewTab ? '_blank' : undefined}
rel={destination?.openInNewTab ? 'noopener noreferrer' : undefined}
className="focus-outline min-w-0 flex-1 rounded-6 before:absolute before:inset-0 before:content-['']"
>
<Typography
Expand Down
50 changes: 1 addition & 49 deletions packages/shared/src/components/quest/QuestButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { useAuthContext } from '../../contexts/AuthContext';
import { useLogContext } from '../../contexts/LogContext';
import { useSettingsContext } from '../../contexts/SettingsContext';
import { plusUrl, webappUrl } from '../../lib/constants';
import { agentsHighlightsPath } from '../../lib/links';
import { getQuestDestination } from './questDestinations';
import Link from '../utilities/Link';
import { AuthTriggers } from '../../lib/auth';
import { LogEvent, TargetId, TargetType } from '../../lib/log';
Expand Down Expand Up @@ -96,54 +96,6 @@ type QuestLevelFireworkParticle = {
hue: number;
};

const HOT_TAKES_MODAL_PATH = '/?openModal=hottakes';

const getQuestDestination = (
quest: UserQuest['quest'],
): QuestDestination | null => {
if (quest.eventType === 'post_share') {
if (quest.description === 'Create a shared link post') {
return { label: 'Create post', path: '/squads/create' };
}

return { label: 'Feed', path: '/' };
}

switch (quest.eventType) {
case 'read_post':
case 'post_upvote':
case 'award_given':
case 'share_post_click':
case 'comment_upvote':
case 'comment_create':
case 'bookmark_post':
return { label: 'Feed', path: '/' };
case 'brief_read':
return { label: 'Briefs', path: '/briefing' };
case 'hot_take_vote':
case 'hot_take_create':
return { label: 'Hot takes', path: HOT_TAKES_MODAL_PATH };
case 'user_follow':
return { label: 'Leaderboards', path: '/users' };
case 'view_user_profile':
return { label: 'Profiles', path: '/users' };
case 'visit_arena':
return { label: 'Happening Now', path: agentsHighlightsPath };
case 'visit_explore_page':
return { label: 'Explore', path: '/posts' };
case 'visit_discussions_page':
return { label: 'Discuss', path: '/discussed' };
case 'visit_read_it_later_page':
return { label: 'Later', path: '/bookmarks/later' };
case 'feedback_submit':
return { label: 'Feedback', path: '/settings/feedback' };
case 'squad_join':
return { label: 'Squads', path: '/squads/discover' };
default:
return null;
}
};

export const QuestSection = ({
title,
quests,
Expand Down
51 changes: 51 additions & 0 deletions packages/shared/src/components/quest/questDestinations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { UserQuest } from '../../graphql/quests';
import { agentsHighlightsPath } from '../../lib/links';
import type { QuestDestination } from './QuestCard';

export const HOT_TAKES_MODAL_PATH = '/?openModal=hottakes';

export const getQuestDestination = (
quest: UserQuest['quest'],
): QuestDestination | null => {
if (quest.eventType === 'post_share') {
if (quest.description === 'Create a shared link post') {
return { label: 'Create post', path: '/squads/create' };
}

return { label: 'Feed', path: '/' };
}

switch (quest.eventType) {
case 'read_post':
case 'post_upvote':
case 'award_given':
case 'share_post_click':
case 'comment_upvote':
case 'comment_create':
case 'bookmark_post':
return { label: 'Feed', path: '/' };
case 'brief_read':
return { label: 'Briefs', path: '/briefing' };
case 'hot_take_vote':
case 'hot_take_create':
return { label: 'Hot takes', path: HOT_TAKES_MODAL_PATH };
case 'user_follow':
return { label: 'Leaderboards', path: '/users' };
case 'view_user_profile':
return { label: 'Profiles', path: '/users' };
case 'visit_arena':
return { label: 'Happening Now', path: agentsHighlightsPath };
case 'visit_explore_page':
return { label: 'Explore', path: '/posts' };
case 'visit_discussions_page':
return { label: 'Discuss', path: '/discussed' };
case 'visit_read_it_later_page':
return { label: 'Later', path: '/bookmarks/later' };
case 'feedback_submit':
return { label: 'Feedback', path: '/settings/feedback' };
case 'squad_join':
return { label: 'Squads', path: '/squads/discover' };
default:
return null;
}
};
Loading
Loading