From 3edbc6ff1eca5eac686520294913ffe35d2718a4 Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Thu, 9 Apr 2026 15:38:29 +0200 Subject: [PATCH 1/5] feat: add bottom-left help guide widget for contextual user guidance Introduces a floating help widget (bottom-left) that surfaces personalized tips, new features, and next actions. Users can browse items with prev/next navigation, click CTAs to complete them, and minimize to a compact "?" button with a popover showing pending/completed items. Uses mock data for demo. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/shared/src/components/MainLayout.tsx | 2 + .../src/components/help/HelpWidget.module.css | 188 +++++++ .../shared/src/components/help/HelpWidget.tsx | 458 ++++++++++++++++++ .../src/components/help/mockHelpGuideData.ts | 60 +++ 4 files changed, 708 insertions(+) create mode 100644 packages/shared/src/components/help/HelpWidget.module.css create mode 100644 packages/shared/src/components/help/HelpWidget.tsx create mode 100644 packages/shared/src/components/help/mockHelpGuideData.ts diff --git a/packages/shared/src/components/MainLayout.tsx b/packages/shared/src/components/MainLayout.tsx index 4af27936ce..ae62f3f249 100644 --- a/packages/shared/src/components/MainLayout.tsx +++ b/packages/shared/src/components/MainLayout.tsx @@ -33,6 +33,7 @@ import PlusMobileEntryBanner from './banners/PlusMobileEntryBanner'; import usePlusEntry from '../hooks/usePlusEntry'; import { SearchProvider } from '../contexts/search/SearchContext'; import { FeedbackWidget } from './feedback'; +import { HelpWidget } from './help/HelpWidget'; const GoBackHeaderMobile = dynamic( () => @@ -215,6 +216,7 @@ function MainLayoutComponent({ )} {children} + ); diff --git a/packages/shared/src/components/help/HelpWidget.module.css b/packages/shared/src/components/help/HelpWidget.module.css new file mode 100644 index 0000000000..eba6a7d802 --- /dev/null +++ b/packages/shared/src/components/help/HelpWidget.module.css @@ -0,0 +1,188 @@ +/* ── Card enter / exit ───────────────────────── */ +@keyframes helpCardSlideIn { + from { + opacity: 0; + transform: translateY(12px) scale(0.96); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes helpCardSlideOut { + from { + opacity: 1; + transform: translateY(0) scale(1); + } + to { + opacity: 0; + transform: translateY(8px) scale(0.97); + } +} + +.cardEnter { + animation: helpCardSlideIn 0.35s cubic-bezier(0.16, 1, 0.3, 1) both; +} + +.cardExit { + animation: helpCardSlideOut 0.2s ease-in both; + pointer-events: none; +} + +/* ── Popover enter ──────────────────────────── */ +@keyframes helpPopoverSlideUp { + from { + opacity: 0; + transform: translateY(6px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.popoverEnter { + animation: helpPopoverSlideUp 0.25s cubic-bezier(0.16, 1, 0.3, 1) both; +} + +/* ── Accent gradient bar ────────────────────── */ +.accentBar { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient( + 90deg, + var(--theme-accent-cabbage-default), + var(--theme-accent-blueCheese-default), + var(--theme-accent-onion-default) + ); + background-size: 200% 100%; + animation: helpGradientShift 4s ease-in-out infinite; +} + +@keyframes helpGradientShift { + 0%, + 100% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } +} + +/* ── Card hover ─────────────────────────────── */ +.card { + transition: box-shadow 0.3s ease, border-color 0.3s ease; +} + +.card:hover { + box-shadow: + 0 8px 30px rgba(0, 0, 0, 0.12), + 0 2px 8px rgba(0, 0, 0, 0.08); + border-color: var(--theme-border-subtlest-secondary); +} + +/* ── Icon soft glow ─────────────────────────── */ +.iconGlow { + position: absolute; + inset: -4px; + border-radius: 14px; + background: var(--theme-accent-cabbage-default); + opacity: 0.08; + filter: blur(8px); +} + +/* ── Help "?" button ────────────────────────── */ +.helpButton { + transition: + transform 0.2s ease, + box-shadow 0.3s ease; +} + +.helpButtonGlow { + position: absolute; + inset: -2px; + border-radius: 50%; + background: linear-gradient( + 135deg, + var(--theme-accent-cabbage-default), + var(--theme-accent-blueCheese-default) + ); + opacity: 0; + transition: opacity 0.3s ease; + filter: blur(6px); +} + +.helpButton:hover .helpButtonGlow { + opacity: 0.2; +} + +/* ── Notification dot pulse ─────────────────── */ +@keyframes helpDotPulse { + 0%, + 100% { + box-shadow: 0 0 0 0 var(--theme-accent-bacon-default); + } + 50% { + box-shadow: 0 0 0 4px transparent; + } +} + +.notificationDot { + animation: helpDotPulse 2s ease-in-out infinite; +} + +/* ── Popover highlight gradient (subtle) ────── */ +.popoverHighlight { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 100%; + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--theme-accent-cabbage-default) 4%, transparent), + transparent + ); + pointer-events: none; +} + +/* ── CTA button shimmer ─────────────────────── */ +@keyframes helpCtaShimmer { + 0% { + background-position: -200% center; + } + 100% { + background-position: 200% center; + } +} + +.ctaButton { + position: relative; + overflow: hidden; +} + +.ctaButton::after { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient( + 90deg, + transparent 30%, + rgba(255, 255, 255, 0.12) 50%, + transparent 70% + ); + background-size: 200% 100%; + animation: helpCtaShimmer 3s ease-in-out infinite; + pointer-events: none; +} + +/* ── Tag subtle animation ───────────────────── */ +.tag { + transition: + background-color 0.2s ease, + color 0.2s ease; +} diff --git a/packages/shared/src/components/help/HelpWidget.tsx b/packages/shared/src/components/help/HelpWidget.tsx new file mode 100644 index 0000000000..de8e782118 --- /dev/null +++ b/packages/shared/src/components/help/HelpWidget.tsx @@ -0,0 +1,458 @@ +import type { ReactElement } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import classNames from 'classnames'; +import { useAuthContext } from '../../contexts/AuthContext'; +import { useViewSize, ViewSize } from '../../hooks/useViewSize'; +import { Button, ButtonSize, ButtonVariant } from '../buttons/Button'; +import { ArrowIcon } from '../icons/Arrow'; +import { MiniCloseIcon } from '../icons/MiniClose'; +import { SparkleIcon } from '../icons/Sparkle'; +import type { HelpGuideItem } from './mockHelpGuideData'; +import { mockHelpGuideItems } from './mockHelpGuideData'; +import styles from './HelpWidget.module.css'; + +type WidgetState = 'expanded' | 'minimized'; + +const COMPLETED_ITEMS_KEY = 'help_widget_completed'; +const WIDGET_STATE_KEY = 'help_widget_state'; + +function getCompletedIds(): string[] { + try { + return JSON.parse(localStorage.getItem(COMPLETED_ITEMS_KEY) || '[]'); + } catch { + return []; + } +} + +function saveCompletedIds(ids: string[]): void { + localStorage.setItem(COMPLETED_ITEMS_KEY, JSON.stringify(ids)); +} + +function getSavedState(): WidgetState { + return (localStorage.getItem(WIDGET_STATE_KEY) as WidgetState) || 'expanded'; +} + +export function HelpWidget(): ReactElement | null { + const { user } = useAuthContext(); + const isMobile = useViewSize(ViewSize.MobileL); + const [widgetState, setWidgetState] = useState('expanded'); + const [completedIds, setCompletedIds] = useState([]); + const [activeIndex, setActiveIndex] = useState(0); + const [popoverOpen, setPopoverOpen] = useState(false); + const [isExiting, setIsExiting] = useState(false); + const popoverRef = useRef(null); + const buttonRef = useRef(null); + + useEffect(() => { + setCompletedIds(getCompletedIds()); + setWidgetState(getSavedState()); + }, []); + + const pendingItems = mockHelpGuideItems.filter( + (item) => !completedIds.includes(item.id), + ); + const pendingCount = pendingItems.length; + const activeItem = mockHelpGuideItems[activeIndex]; + + const isCompleted = activeItem + ? completedIds.includes(activeItem.id) + : false; + + const completeItem = useCallback( + (item: HelpGuideItem) => { + if (completedIds.includes(item.id)) { + return; + } + + const newCompleted = [...completedIds, item.id]; + setCompletedIds(newCompleted); + saveCompletedIds(newCompleted); + }, + [completedIds], + ); + + const handleCtaClick = useCallback( + (item: HelpGuideItem) => { + completeItem(item); + if (item.ctaUrl) { + window.location.href = item.ctaUrl; + } + }, + [completeItem], + ); + + const goNext = useCallback(() => { + setIsExiting(true); + setTimeout(() => { + setActiveIndex((prev) => (prev + 1) % mockHelpGuideItems.length); + setIsExiting(false); + }, 150); + }, []); + + const goPrev = useCallback(() => { + setIsExiting(true); + setTimeout(() => { + setActiveIndex( + (prev) => + (prev - 1 + mockHelpGuideItems.length) % mockHelpGuideItems.length, + ); + setIsExiting(false); + }, 150); + }, []); + + const goToIndex = useCallback( + (index: number) => { + if (index === activeIndex) { + return; + } + + setIsExiting(true); + setTimeout(() => { + setActiveIndex(index); + setIsExiting(false); + }, 150); + }, + [activeIndex], + ); + + const minimize = useCallback(() => { + setIsExiting(true); + setTimeout(() => { + setWidgetState('minimized'); + localStorage.setItem(WIDGET_STATE_KEY, 'minimized'); + setPopoverOpen(false); + setIsExiting(false); + }, 200); + }, []); + + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if ( + popoverRef.current && + !popoverRef.current.contains(e.target as Node) && + buttonRef.current && + !buttonRef.current.contains(e.target as Node) + ) { + setPopoverOpen(false); + } + }; + + if (popoverOpen) { + document.addEventListener('mousedown', handleClickOutside); + } + + return () => document.removeEventListener('mousedown', handleClickOutside); + }, [popoverOpen]); + + if (!user || isMobile) { + return null; + } + + if (widgetState === 'minimized') { + return ( +
+ {popoverOpen && ( + { + setActiveIndex(index); + setWidgetState('expanded'); + localStorage.setItem(WIDGET_STATE_KEY, 'expanded'); + setPopoverOpen(false); + }} + onCtaClick={handleCtaClick} + /> + )} + +
+ ); + } + + if (!activeItem) { + return null; + } + + const showNav = mockHelpGuideItems.length > 1; + + return ( +
+
+ {/* Gradient accent bar */} +
+ + {/* Close button */} + + )} + {showNav && ( +
+
+ )} +
+
+ + {/* Step indicator — clickable */} + {showNav && ( +
+ {mockHelpGuideItems.map((item, index) => ( +
+ )} +
+ + ); +} + +interface PopoverMenuProps { + items: HelpGuideItem[]; + completedIds: string[]; + onItemClick: (item: HelpGuideItem, index: number) => void; + onCtaClick: (item: HelpGuideItem) => void; +} + +const PopoverMenu = React.forwardRef( + function PopoverMenu({ items, completedIds, onItemClick, onCtaClick }, ref) { + const pending = items.filter((item) => !completedIds.includes(item.id)); + const completed = items.filter((item) => completedIds.includes(item.id)); + + return ( +
+ {/* Pending items */} + {pending.length > 0 && ( +
+
+
+ + Suggested for you + +
    + {pending.map((item) => { + const index = items.indexOf(item); + return ( +
  • + +
  • + ); + })} +
+
+
+ )} + + {/* Completed items */} + {completed.length > 0 && ( +
0 && + 'border-t border-border-subtlest-tertiary', + )} + > + + Completed + +
    + {completed.map((item) => ( +
  • + + ✓ + + + {item.title} + +
  • + ))} +
+
+ )} + + {/* All done state */} + {items.length === 0 && ( +
+ + + You're all caught up! + +
+ )} + + {/* Dev-only reset */} + {process.env.NODE_ENV !== 'production' && ( +
+ +
+ )} +
+ ); + }, +); diff --git a/packages/shared/src/components/help/mockHelpGuideData.ts b/packages/shared/src/components/help/mockHelpGuideData.ts new file mode 100644 index 0000000000..c6c18188e3 --- /dev/null +++ b/packages/shared/src/components/help/mockHelpGuideData.ts @@ -0,0 +1,60 @@ +export interface HelpGuideItem { + id: string; + title: string; + description: string; + ctaLabel: string; + ctaUrl?: string; + tag?: string; + isNew?: boolean; +} + +/** + * Mock data for demo purposes. + * In production this would come from an API based on user state. + */ +export const mockHelpGuideItems: HelpGuideItem[] = [ + { + id: 'customize-feed', + title: 'Customize your feed', + description: + 'Pick your favorite tags and sources to get a feed tailored to your interests. The more you customize, the better your feed gets.', + ctaLabel: 'Go to feed settings', + ctaUrl: '/feeds/new', + tag: 'Action', + }, + { + id: 'smart-prompts', + title: 'Smart Prompts are here!', + description: + 'Ask follow-up questions on any post with AI-powered Smart Prompts. Available for Plus members.', + ctaLabel: 'Try it now', + isNew: true, + tag: 'New', + }, + { + id: 'squads', + title: 'Create your first Squad', + description: + 'Squads are private groups where you and your team can share and discuss articles together.', + ctaLabel: 'Create a Squad', + ctaUrl: '/squads/new', + tag: 'Getting started', + }, + { + id: 'bookmarks', + title: 'Organize with Bookmark Folders', + description: + 'Save posts for later and organize them into folders so you never lose track of great content.', + ctaLabel: 'View bookmarks', + ctaUrl: '/bookmarks', + }, + { + id: 'streak', + title: 'Keep your reading streak going!', + description: + "You're on a 3-day streak. Read one more post today to keep it alive and unlock streak milestones.", + ctaLabel: 'Browse posts', + ctaUrl: '/', + tag: 'Streak', + }, +]; From b7b83b6bc789051f7ce078b8d3ef9e2e3b71deb7 Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Thu, 9 Apr 2026 15:47:41 +0200 Subject: [PATCH 2/5] fix: help widget background opacity and z-index Use opaque bg-background-popover for expanded card readability. Glass effect (92% opacity + backdrop-blur) kept on ? button and popover. Bump z-index to z-max to stay above sidebar. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/shared/src/components/help/HelpWidget.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/shared/src/components/help/HelpWidget.tsx b/packages/shared/src/components/help/HelpWidget.tsx index de8e782118..91c6737cc9 100644 --- a/packages/shared/src/components/help/HelpWidget.tsx +++ b/packages/shared/src/components/help/HelpWidget.tsx @@ -150,7 +150,7 @@ export function HelpWidget(): ReactElement | null { if (widgetState === 'minimized') { return ( -
+
{popoverOpen && ( setPopoverOpen(!popoverOpen)} @@ -207,7 +207,7 @@ export function HelpWidget(): ReactElement | null { return (
@@ -215,7 +215,7 @@ export function HelpWidget(): ReactElement | null { className={classNames( styles.card, 'relative overflow-hidden rounded-16 border border-border-subtlest-tertiary', - 'bg-surface-float/95 shadow-2 backdrop-blur-xl', + 'bg-background-popover shadow-2', )} > {/* Gradient accent bar */} @@ -357,7 +357,7 @@ const PopoverMenu = React.forwardRef( className={classNames( styles.popoverEnter, 'mb-2 w-80 overflow-hidden rounded-16 border border-border-subtlest-tertiary', - 'bg-surface-float/95 shadow-2 backdrop-blur-xl', + 'bg-background-popover/[0.92] shadow-2 backdrop-blur-xl', )} > {/* Pending items */} From bee1e24d806737baa64a51d8b7c42d7fb9d1bfce Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Thu, 9 Apr 2026 16:09:15 +0200 Subject: [PATCH 3/5] refactor: move help widget into sidebar bottom Move HelpWidget from floating fixed position into the sidebar, pinned below the scroll area. Collapsed sidebar shows icon trigger, expanded shows inline preview card. Detail card and popover pop out to the right. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/shared/src/components/MainLayout.tsx | 2 - .../src/components/help/HelpWidget.module.css | 36 +- .../shared/src/components/help/HelpWidget.tsx | 438 ++++++++++++------ .../src/components/sidebar/SidebarDesktop.tsx | 12 + 4 files changed, 316 insertions(+), 172 deletions(-) diff --git a/packages/shared/src/components/MainLayout.tsx b/packages/shared/src/components/MainLayout.tsx index ae62f3f249..4af27936ce 100644 --- a/packages/shared/src/components/MainLayout.tsx +++ b/packages/shared/src/components/MainLayout.tsx @@ -33,7 +33,6 @@ import PlusMobileEntryBanner from './banners/PlusMobileEntryBanner'; import usePlusEntry from '../hooks/usePlusEntry'; import { SearchProvider } from '../contexts/search/SearchContext'; import { FeedbackWidget } from './feedback'; -import { HelpWidget } from './help/HelpWidget'; const GoBackHeaderMobile = dynamic( () => @@ -216,7 +215,6 @@ function MainLayoutComponent({ )} {children} -
); diff --git a/packages/shared/src/components/help/HelpWidget.module.css b/packages/shared/src/components/help/HelpWidget.module.css index eba6a7d802..a69be0c291 100644 --- a/packages/shared/src/components/help/HelpWidget.module.css +++ b/packages/shared/src/components/help/HelpWidget.module.css @@ -2,22 +2,22 @@ @keyframes helpCardSlideIn { from { opacity: 0; - transform: translateY(12px) scale(0.96); + transform: translateX(-8px) scale(0.96); } to { opacity: 1; - transform: translateY(0) scale(1); + transform: translateX(0) scale(1); } } @keyframes helpCardSlideOut { from { opacity: 1; - transform: translateY(0) scale(1); + transform: translateX(0) scale(1); } to { opacity: 0; - transform: translateY(8px) scale(0.97); + transform: translateX(-6px) scale(0.97); } } @@ -95,29 +95,27 @@ filter: blur(8px); } -/* ── Help "?" button ────────────────────────── */ -.helpButton { +/* ── Inline sidebar card ────────────────────── */ +.inlineCard { transition: - transform 0.2s ease, - box-shadow 0.3s ease; + border-color 0.2s ease, + background-color 0.2s ease; } -.helpButtonGlow { +.inlineAccent { position: absolute; - inset: -2px; - border-radius: 50%; + top: 0; + left: 0; + right: 0; + height: 2px; background: linear-gradient( - 135deg, + 90deg, var(--theme-accent-cabbage-default), var(--theme-accent-blueCheese-default) ); - opacity: 0; - transition: opacity 0.3s ease; - filter: blur(6px); -} - -.helpButton:hover .helpButtonGlow { - opacity: 0.2; + background-size: 200% 100%; + animation: helpGradientShift 4s ease-in-out infinite; + border-radius: 12px 12px 0 0; } /* ── Notification dot pulse ─────────────────── */ diff --git a/packages/shared/src/components/help/HelpWidget.tsx b/packages/shared/src/components/help/HelpWidget.tsx index 91c6737cc9..0ed1837886 100644 --- a/packages/shared/src/components/help/HelpWidget.tsx +++ b/packages/shared/src/components/help/HelpWidget.tsx @@ -2,7 +2,6 @@ import type { ReactElement } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import classNames from 'classnames'; import { useAuthContext } from '../../contexts/AuthContext'; -import { useViewSize, ViewSize } from '../../hooks/useViewSize'; import { Button, ButtonSize, ButtonVariant } from '../buttons/Button'; import { ArrowIcon } from '../icons/Arrow'; import { MiniCloseIcon } from '../icons/MiniClose'; @@ -32,16 +31,21 @@ function getSavedState(): WidgetState { return (localStorage.getItem(WIDGET_STATE_KEY) as WidgetState) || 'expanded'; } -export function HelpWidget(): ReactElement | null { +interface HelpWidgetProps { + sidebarExpanded: boolean; +} + +export function HelpWidget({ + sidebarExpanded, +}: HelpWidgetProps): ReactElement | null { const { user } = useAuthContext(); - const isMobile = useViewSize(ViewSize.MobileL); const [widgetState, setWidgetState] = useState('expanded'); const [completedIds, setCompletedIds] = useState([]); const [activeIndex, setActiveIndex] = useState(0); const [popoverOpen, setPopoverOpen] = useState(false); const [isExiting, setIsExiting] = useState(false); const popoverRef = useRef(null); - const buttonRef = useRef(null); + const triggerRef = useRef(null); useEffect(() => { setCompletedIds(getCompletedIds()); @@ -130,8 +134,8 @@ export function HelpWidget(): ReactElement | null { if ( popoverRef.current && !popoverRef.current.contains(e.target as Node) && - buttonRef.current && - !buttonRef.current.contains(e.target as Node) + triggerRef.current && + !triggerRef.current.contains(e.target as Node) ) { setPopoverOpen(false); } @@ -144,13 +148,46 @@ export function HelpWidget(): ReactElement | null { return () => document.removeEventListener('mousedown', handleClickOutside); }, [popoverOpen]); - if (!user || isMobile) { + if (!user) { return null; } + // Minimized: show as sidebar item at the bottom if (widgetState === 'minimized') { return ( -
+
+ + + {/* Popover — positioned to the right of sidebar */} {popoverOpen && ( )} +
+ ); + } + + if (!activeItem) { + return null; + } + + const showNav = mockHelpGuideItems.length > 1; + + // Expanded: show card anchored to sidebar bottom, popping to the right + return ( +
+ {/* Collapsed sidebar: just show the sparkle icon as trigger */} + {!sidebarExpanded && ( + )} + + {/* Expanded sidebar: show inline preview card */} + {sidebarExpanded && ( + -
- ); - } + )} - if (!activeItem) { - return null; - } + {/* Detail card — pops out to the right of the sidebar */} + {popoverOpen && ( +
+ setPopoverOpen(false)} + onMinimize={() => { + minimize(); + setPopoverOpen(false); + }} + onNext={goNext} + onPrev={goPrev} + onGoToIndex={goToIndex} + /> +
+ )} +
+ ); +} - const showNav = mockHelpGuideItems.length > 1; +interface DetailCardProps { + activeItem: HelpGuideItem; + activeIndex: number; + isCompleted: boolean; + showNav: boolean; + completedIds: string[]; + onCtaClick: (item: HelpGuideItem) => void; + onClose: () => void; + onMinimize: () => void; + onNext: () => void; + onPrev: () => void; + onGoToIndex: (index: number) => void; +} +function DetailCard({ + activeItem, + activeIndex, + isCompleted, + showNav, + completedIds, + onCtaClick, + onClose, + onMinimize, + onNext, + onPrev, + onGoToIndex, +}: DetailCardProps): ReactElement { return (
-
- {/* Gradient accent bar */} -
- - {/* Close button */} - + {activeItem.tag} + )} - {showNav && ( -
-
+ {isCompleted && ( + + Done + )}
- {/* Step indicator — clickable */} - {showNav && ( -
- {mockHelpGuideItems.map((item, index) => ( - + )} + {showNav && ( +
+
- )} + + {activeIndex + 1}/{mockHelpGuideItems.length} + +
+ )} +
+ + {/* Step indicator — clickable */} + {showNav && ( +
+ {mockHelpGuideItems.map((item, index) => ( +
+ )}
); } @@ -344,10 +473,14 @@ interface PopoverMenuProps { completedIds: string[]; onItemClick: (item: HelpGuideItem, index: number) => void; onCtaClick: (item: HelpGuideItem) => void; + className?: string; } const PopoverMenu = React.forwardRef( - function PopoverMenu({ items, completedIds, onItemClick, onCtaClick }, ref) { + function PopoverMenu( + { items, completedIds, onItemClick, onCtaClick, className }, + ref, + ) { const pending = items.filter((item) => !completedIds.includes(item.id)); const completed = items.filter((item) => completedIds.includes(item.id)); @@ -356,8 +489,9 @@ const PopoverMenu = React.forwardRef( ref={ref} className={classNames( styles.popoverEnter, - 'mb-2 w-80 overflow-hidden rounded-16 border border-border-subtlest-tertiary', - 'bg-background-popover/[0.92] shadow-2 backdrop-blur-xl', + 'z-modal w-80 overflow-hidden rounded-16 border border-border-subtlest-tertiary', + 'bg-background-popover shadow-2', + className, )} > {/* Pending items */} @@ -404,8 +538,7 @@ const PopoverMenu = React.forwardRef(
0 && - 'border-t border-border-subtlest-tertiary', + pending.length > 0 && 'border-t border-border-subtlest-tertiary', )} > @@ -413,7 +546,10 @@ const PopoverMenu = React.forwardRef(
    {completed.map((item) => ( -
  • +
  • diff --git a/packages/shared/src/components/sidebar/SidebarDesktop.tsx b/packages/shared/src/components/sidebar/SidebarDesktop.tsx index ec062985c5..370b24197c 100644 --- a/packages/shared/src/components/sidebar/SidebarDesktop.tsx +++ b/packages/shared/src/components/sidebar/SidebarDesktop.tsx @@ -13,6 +13,7 @@ import { CreatePostButton } from '../post/write'; import { ButtonSize } from '../buttons/Button'; import { BookmarkSection } from './sections/BookmarkSection'; import { NetworkSection } from './sections/NetworkSection'; +import { HelpWidget } from '../help/HelpWidget'; type SidebarDesktopProps = { activePage?: string; @@ -110,6 +111,17 @@ export const SidebarDesktop = ({ /> + + {/* Help guide — pinned to sidebar bottom */} +
    + +
    ); }; From 9f4ed02727f8bfdd43cd6b264994bd324590c7c8 Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Thu, 9 Apr 2026 16:13:07 +0200 Subject: [PATCH 4/5] fix: make help widget visible at sidebar bottom Override SidebarScrollWrapper h-full with flex-1 min-h-0 so the help widget pinned below it gets layout space. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/shared/src/components/sidebar/SidebarDesktop.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/src/components/sidebar/SidebarDesktop.tsx b/packages/shared/src/components/sidebar/SidebarDesktop.tsx index 370b24197c..8a40971a1b 100644 --- a/packages/shared/src/components/sidebar/SidebarDesktop.tsx +++ b/packages/shared/src/components/sidebar/SidebarDesktop.tsx @@ -55,7 +55,7 @@ export const SidebarDesktop = ({ featureTheme && 'bg-transparent', )} > - +