Skip to content

fix(sidebar/main): impl modal selector and fix logos and items#74

Merged
phasehumans merged 2 commits into
mainfrom
chaitanya/fix-sidebar-items
May 6, 2026
Merged

fix(sidebar/main): impl modal selector and fix logos and items#74
phasehumans merged 2 commits into
mainfrom
chaitanya/fix-sidebar-items

Conversation

@phasehumans

@phasehumans phasehumans commented May 6, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

Release Notes

New Features

  • Added model selection dropdown to prompt footer with multiple model options (Auto, Claude, GPT, Gemini, DeepSeek).
  • Added Settings action to notifications popover with menu controls and keyboard navigation.

Improvements

  • Updated sidebar with improved user avatar display and navigation icons.
  • Enhanced navigation styling with updated color palette and dynamic styling.
  • Improved user profile information display across navigation components.
  • Updated branding assets and logo displays throughout the application.

UI Updates

  • Refined animation behavior for import integration UI.
  • Streamlined empty project state visualization.

@coderabbitai

coderabbitai Bot commented May 6, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR refreshes the platform branding from Canvas/Phase to December across auth, navigation, and UI components. It adds settings support to the notifications popover, introduces model selection to the prompt footer, and updates icon collections and styling throughout.

Changes

Platform Branding and Navigation Refresh

Layer / File(s) Summary
Icon System
web/src/shared/components/ui/Icons.tsx
Adds new DecemberLogo and Bookmark icons; replaces PhaseLogoSvg with simplified PhaseLogo; updates Layers (dashed stroke), UserCircle (filled variant), and DocsBook icon definitions.
Auth UI
web/src/features/auth/components/AuthModalAuthStep.tsx, web/src/features/auth/components/AuthModalOtpStep.tsx
Replaces Icons.CanvasIcon with Icons.DecemberLogo in authentication modal headers; updates container styling for text color.
Sidebar Navigation
web/src/features/navigation/components/SidebarHeader.tsx, web/src/features/navigation/components/Sidebar.tsx
Switches sidebar header logo from Canvas to DecemberLogo; replaces Projects/Templates icons (Grid/Layout → Folder/Bookmark); introduces dynamic class composition via cn utility for active/inactive tab states; updates "Recent Projects" section styling with larger, medium-weight labels.
Sidebar Appearance
web/src/features/navigation/components/SidebarNavItem.tsx
Color palette adjustment: non-active text and icon colors updated from #969593 to #8F8E8D.
Navigation Features
web/src/features/navigation/components/NotificationsPopover.tsx
Adds optional onSettings callback to popover props; introduces internal state for menu visibility, portal-based settings menu anchored to popover, outside-click and keyboard handlers, and dedicated Settings/Delete-all-read actions.
Sidebar Footer
web/src/features/navigation/components/SidebarFooter.tsx
Adds 28x28 user avatar display; expands name resolution with multi-source fallback (user.name, profile.name, quickInfo fields); passes onSettings callback to NotificationsPopover.
User Profile Menu
web/src/features/navigation/components/UserProfilePopover.tsx, web/src/features/profile/components/ProfileSettings.tsx
Updates icons across user profile and settings menus; swaps BookText for Icons.DocsBook in documentation links; reorders menu items (Profile, Settings, Feedback, Pricing, Documentation); adjusts popover width to 280px.
Hero and Home
web/src/features/home/components/HomeHero.tsx, web/src/features/home/components/HomeHeader.tsx
Switches import options animation from opacity-0 to visible animate-in fade-in duration-300; updates second social link repository URL from phasehumans.dev to december.
Prompt Footer
web/src/shared/components/ui/PromptFooter.tsx
Introduces model selection dropdown with local state, refs, and outside-click handling; displays selected model and allows switching between Auto, Claude, GPT, Gemini, and DeepSeek variants.
Assets
web/src/features/projects/components/ProjectListView.tsx
Removes decorative SVG paths from EmptyProjectsState illustration.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • phasehumans/december#72: Modifies sidebar navigation components (Sidebar.tsx) with related icon and styling changes.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title broadly references sidebar items and modal selector implementation, which aligns with multiple changes across navigation and authentication components, but doesn't clearly highlight the primary focus or scope. Consider a more specific title that highlights the main objective—either the modal selector feature in PromptFooter, the logo/branding updates (December Logo), or the sidebar/navigation restructuring—to provide clearer intent.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chaitanya/fix-sidebar-items

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
web/src/features/navigation/components/SidebarFooter.tsx (2)

78-79: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Hardcoded personal email as a default user value.

'dev.chaitanyasonawane@gmail.com' is shipped as the default userEmail and 'phasehuman' as the default userName. A real personal address baked into a public component is a PII/branding leak — it will display on any code path where profile?.email is missing, including unauthenticated/loading states. Use a placeholder ("you@example.com" or empty string) and let upstream decide what to render.

The same value is also hardcoded in UserProfilePopover.tsx at the prop default; consider scrubbing both.


86-91: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Wiring onSettings={onProfile} conflates settings with profile.

The NotificationsPopover "Settings" entry will invoke onProfile, which from the rest of this component appears to open a profile flow rather than a notification-settings flow. If that is a temporary placeholder, leave a TODO; if it is intentional, naming the callback onSettings for an action that opens profile is going to be confusing for the next reader. Same applies to the onSettings={onProfile} on UserProfilePopover and ProfileCardModal above.

web/src/features/navigation/components/Sidebar.tsx (1)

11-11: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid any on the public user prop.

user?: any defeats the type system at the boundary between Sidebar and its consumers. Define the actual shape (or import the existing user type) so downstream usage like user?.name in SidebarFooter is checked. As per coding guidelines, "TypeScript must be strict; prefer Zod schemas for API boundaries".

Suggested typing
-const Sidebar: React.FC<SidebarProps & { user?: any }> = ({
+type SidebarUser = { name?: string }
+const Sidebar: React.FC<SidebarProps & { user?: SidebarUser }> = ({
web/src/shared/components/ui/PromptFooter.tsx (1)

6-21: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Selected model is never propagated to the submission path.

selectedModel is local state and onSubmit takes no arguments, so whichever model the user picks is dropped on the floor at send time. This makes the new selector decorative until the API is extended (e.g. onSubmit: (model: string) => void or by lifting the state into the parent that owns the prompt request). Worth resolving in this PR or the feature is non-functional.

Also applies to: 145-156


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f510d46f-669a-4ba8-8437-48ee9d639720

📥 Commits

Reviewing files that changed from the base of the PR and between 0a0e38b and dc63e2b.

📒 Files selected for processing (14)
  • web/src/features/auth/components/AuthModalAuthStep.tsx
  • web/src/features/auth/components/AuthModalOtpStep.tsx
  • web/src/features/home/components/HomeHeader.tsx
  • web/src/features/home/components/HomeHero.tsx
  • web/src/features/navigation/components/NotificationsPopover.tsx
  • web/src/features/navigation/components/Sidebar.tsx
  • web/src/features/navigation/components/SidebarFooter.tsx
  • web/src/features/navigation/components/SidebarHeader.tsx
  • web/src/features/navigation/components/SidebarNavItem.tsx
  • web/src/features/navigation/components/UserProfilePopover.tsx
  • web/src/features/profile/components/ProfileSettings.tsx
  • web/src/features/projects/components/ProjectListView.tsx
  • web/src/shared/components/ui/Icons.tsx
  • web/src/shared/components/ui/PromptFooter.tsx
💤 Files with no reviewable changes (1)
  • web/src/features/projects/components/ProjectListView.tsx

Comment on lines 37 to 49
<a
href="https://x.com/phasehumans"
target="_blank"
rel="noopener noreferrer"
className="text-textMuted hover:text-textMain transition-colors flex items-center justify-center p-1"
>
<Icons.XLogo className="w-[15px] h-[15px]" />
</a>
<a
href="https://github.com/phasehumans/phasehumans.dev"
href="https://github.com/phasehumans/december"
target="_blank"
rel="noopener noreferrer"
className="text-textMuted hover:text-textMain transition-colors flex items-center justify-center p-1"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add accessible names to the social links.

Both anchors are icon-only, so assistive tech gets an unnamed control here. Add aria-labels (and optionally mark the SVGs aria-hidden) so the X and GitHub links are discoverable.

Comment on lines 39 to 43
setPosition({
bottom: window.innerHeight - rect.top + 8,
left: rect.left - 260 + rect.width, // align to right or just fixed width. Wait, rect.left is the notification icon. Let's align left to something reasonable or just fixed width of 300. We will set left to rect.left - 250 so it opens to the left.
left: rect.left - 250,
width: 280,
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Nested menu position can desync from the popover.

Line 41 sets left: rect.left - 250, line 109 then clamps with Math.max(10, position.left), but the nested menu at line 130 uses the raw position.left + 296. When the anchor is near the left edge and the popover is shifted to 10, the menu still anchors off the unclamped value, so the visual relationship between popover and menu breaks. Also the menu's top (line 129) is computed from a hardcoded 308 (the popover's height), which silently breaks if the popover height ever changes.

Consider deriving the menu position from the popover's actual rect (e.g. measure popoverRef.current?.getBoundingClientRect() once it is mounted) instead of recomputing from constants.

Also applies to: 109-110, 128-131

Comment on lines +116 to 121
<button
onClick={() => setIsMenuOpen(!isMenuOpen)}
className="text-[#969593] hover:text-[#CBCACA] transition-colors outline-none"
>
<MoreHorizontal className="w-[18px] h-[18px]" />
</button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Icon-only "more" button lacks an accessible name and visible focus state.

The trigger only renders a MoreHorizontal glyph and uses outline-none — keyboard and screen reader users will not know what it does and will not see focus on Tab. Add an aria-label and either keep a focus ring or replace outline-none with a visible focus-visible: style.

Suggested change
-                <button
-                    onClick={() => setIsMenuOpen(!isMenuOpen)}
-                    className="text-[`#969593`] hover:text-[`#CBCACA`] transition-colors outline-none"
-                >
+                <button
+                    type="button"
+                    aria-label="Notification options"
+                    aria-haspopup="menu"
+                    aria-expanded={isMenuOpen}
+                    onClick={() => setIsMenuOpen(!isMenuOpen)}
+                    className="text-[`#969593`] hover:text-[`#CBCACA`] transition-colors focus-visible:ring-2 focus-visible:ring-white/20 rounded"
+                >
                     <MoreHorizontal className="w-[18px] h-[18px]" />
                 </button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
onClick={() => setIsMenuOpen(!isMenuOpen)}
className="text-[#969593] hover:text-[#CBCACA] transition-colors outline-none"
>
<MoreHorizontal className="w-[18px] h-[18px]" />
</button>
<button
type="button"
aria-label="Notification options"
aria-haspopup="menu"
aria-expanded={isMenuOpen}
onClick={() => setIsMenuOpen(!isMenuOpen)}
className="text-[`#969593`] hover:text-[`#CBCACA`] transition-colors focus-visible:ring-2 focus-visible:ring-white/20 rounded"
>
<MoreHorizontal className="w-[18px] h-[18px]" />
</button>

<button className="text-[#969593] hover:text-[#CBCACA] transition-colors">
<button
onClick={() => setIsMenuOpen(!isMenuOpen)}
className="text-[#969593] hover:text-[#CBCACA] transition-colors outline-none"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Tailwind CSS v4 outline-none vs outline-hidden behavior

💡 Result:

In Tailwind CSS v4: 1) outline-none - Sets outline-style: none (removes the focus outline entirely). [1] 2) outline-hidden - Hides the default browser focus outline, but preserves an outline in Windows/forced-colors mode for accessibility. [1] So the “behavior difference” you’re looking for is primarily about forced-colors/high-contrast mode support: use outline-hidden when you want the outline hidden normally but still visible in forced colors; use outline-none when you truly want it removed. [1] Upgrading note: Tailwind changed the meaning/naming between v3 and v4 because the old outline-none behavior was implemented via an invisible outline for forced-colors accessibility. In v4, that behavior was moved to outline-hidden, and outline-none now simply sets outline-style: none. [2][3]

Citations:


🏁 Script executed:

# Find and read the NotificationsPopover.tsx file
find . -name "NotificationsPopover.tsx" -type f

Repository: phasehumans/december

Length of output: 130


🏁 Script executed:

# Check Tailwind version in project configuration
fd -e 'package.json' | head -1 | xargs cat | jq '.devDependencies.tailwindcss, .dependencies.tailwindcss'

Repository: phasehumans/december

Length of output: 46


🏁 Script executed:

# Read the specific lines from the file with context
if [ -f "web/src/features/navigation/components/NotificationsPopover.tsx" ]; then
  sed -n '110,160p' "web/src/features/navigation/components/NotificationsPopover.tsx" | cat -n
fi

Repository: phasehumans/december

Length of output: 3101


🏁 Script executed:

# Read package.json to find Tailwind version
cat package.json | jq '.devDependencies | .tailwindcss, .dependencies | .tailwindcss'

Repository: phasehumans/december

Length of output: 74


🏁 Script executed:

# Check the entire NotificationsPopover.tsx file for focus-related styles
cat "web/src/features/navigation/components/NotificationsPopover.tsx" | grep -i "focus"

Repository: phasehumans/december

Length of output: 46


🏁 Script executed:

# Search for focus-visible patterns in the codebase to see if there's a pattern
rg "focus-visible" web/src/features/navigation/ -A 2

Repository: phasehumans/december

Length of output: 1956


Add focus indicators to match the established codebase pattern.

These buttons have outline-none but lack the focus-visible:ring-2 focus-visible:ring-white/20 styles used throughout the navigation components. Keyboard users need a visible focus indicator. Apply the same focus pattern as SidebarNavItem, SidebarProjectItem, and SidebarSectionHeader.

Also applies to: 139-139, 149-149

Comment on lines +85 to 88
<span className="font-medium text-[13px] transition-colors tracking-tight text-[#8F8E8D] group-hover:text-[#CBCACA] truncate">
{/* @ts-ignore */}
{project.name || project.title}
</span>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

@ts-ignore is masking a real type gap on project.

Suppressing the error means the next person who refactors project won't be told this code expects either name or title. Either widen the Project type to include title (or rename the field), or pick the canonical one and convert at the API boundary. As per coding guidelines, "TypeScript must be strict; prefer Zod schemas for API boundaries".

Comment on lines +10 to +19
DecemberLogo: (props: any) => (
<svg
{...props}
viewBox="5 4 14 16"
fill="none"
stroke="currentColor"
strokeWidth="1"
strokeLinecap="round"
strokeLinejoin="round"
>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# First, let's find and examine the Icons.tsx file
fd -t f "Icons.tsx"

Repository: phasehumans/december

Length of output: 103


🏁 Script executed:

# Read the Icons.tsx file to check the line ranges mentioned
cat -n web/src/shared/components/ui/Icons.tsx | head -50

Repository: phasehumans/december

Length of output: 2339


🏁 Script executed:

# Check lines 124-135 to see the pattern
cat -n web/src/shared/components/ui/Icons.tsx | sed -n '120,140p'

Repository: phasehumans/december

Length of output: 838


🏁 Script executed:

# Check the ProfileSettings.tsx file and line 176
fd -t f "ProfileSettings.tsx" | head -5

Repository: phasehumans/december

Length of output: 120


🏁 Script executed:

# Read ProfileSettings.tsx around line 176
cat -n web/src/features/profile/components/ProfileSettings.tsx | sed -n '170,185p'

Repository: phasehumans/december

Length of output: 1269


🏁 Script executed:

# Search for DocsBook in Icons.tsx
rg -n "DocsBook" web/src/shared/components/ui/Icons.tsx -A 15

Repository: phasehumans/december

Length of output: 640


🏁 Script executed:

# Check a few more of the mentioned line ranges to verify the pattern holds
cat -n web/src/shared/components/ui/Icons.tsx | sed -n '139,160p'

Repository: phasehumans/december

Length of output: 886


🏁 Script executed:

# Check lines 288-299 as mentioned
cat -n web/src/shared/components/ui/Icons.tsx | sed -n '285,305p'

Repository: phasehumans/december

Length of output: 788


🏁 Script executed:

# Check TypeScript configuration to verify strict mode requirement
fd -t f "tsconfig.json" | head -3

Repository: phasehumans/december

Length of output: 117


🏁 Script executed:

# Check the web tsconfig for strict mode
cat web/tsconfig.json

Repository: phasehumans/december

Length of output: 1007


🏁 Script executed:

# Also check the root tsconfig
cat tsconfig.json

Repository: phasehumans/december

Length of output: 869


Move the spread to the end of attributes to allow consumer overrides.

Each icon component spreads props before hardcoded attributes like strokeWidth, width, and height. Since JSX attributes are evaluated left-to-right with later ones overriding earlier ones, the hardcoded values always win. This means strokeWidth={1.5} in ProfileSettings.tsx Line 176 is ignored because DocsBook hardcodes strokeWidth="2" after the spread. Reorder to <svg width="18" height="18" strokeWidth="2" {...props}> to let consumers override defaults.

Also applies to: 124-135, 139-150, 155-166, 170-181, 191-202, 206-217, 288-299

Additionally, replace (props: any) with a properly typed SVG prop interface. The repository enforces strict TypeScript across all .ts and .tsx files, and exported UI components should not use any at their boundaries.

<path d="M53.4,51.4 C58.2,53.0 66.5,56.3 75.3,60.1 C80.6,62.4 84.8,64.3 85.4,64.8 C83.2,70.5 76.5,80.1 69.3,86.6 C64.2,91.3 54.1,95.5 45.4,94.2 C45.3,94.2 44.9,94.1 44.4,94.0 C45.8,80.7 48.0,66.5 53.4,51.4 Z" />
<path d="M49.4,51.6 C44.8,64.5 42.1,77.5 40.5,91.9 C33.1,88.7 26.6,80.1 23.3,71.7 C21.4,66.9 22.0,62.8 23.9,59.5 C25.0,57.7 26.6,55.9 28.5,54.2 C35.4,53.9 42.4,53.0 49.4,51.6 Z" />
<path d="M50.1,11.1 C52.2,18.0 52.4,31.7 51.5,45.7 C44.8,47.1 38.0,48.0 31.4,48.4 C32.1,43.2 34.0,37.8 37.0,32.3 C40.8,25.6 44.9,16.8 50.1,11.1 Z" />
DecemberLogo: (props: any) => (

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

wc -l web/src/shared/components/ui/Icons.tsx

Repository: phasehumans/december

Length of output: 108


🏁 Script executed:

head -50 web/src/shared/components/ui/Icons.tsx

Repository: phasehumans/december

Length of output: 1989


🏁 Script executed:

sed -n '120,130p; 135,145p; 150,160p; 165,175p; 185,195p; 200,210p; 285,295p' web/src/shared/components/ui/Icons.tsx

Repository: phasehumans/december

Length of output: 2413


🏁 Script executed:

grep -n "type IconProps\|interface.*Props" web/src/shared/components/ui/Icons.tsx

Repository: phasehumans/december

Length of output: 46


🏁 Script executed:

sed -n '10p; 124p; 139p; 155p; 170p; 191p; 206p; 288p' web/src/shared/components/ui/Icons.tsx

Repository: phasehumans/december

Length of output: 315


🏁 Script executed:

grep -n "props: any" web/src/shared/components/ui/Icons.tsx | head -20

Repository: phasehumans/december

Length of output: 761


Replace any props with a proper type across all icon components in this file.

The Icons object has multiple components using any for props, which bypasses TypeScript's type checking at a shared UI boundary. Use type IconProps = React.SVGProps<SVGSVGElement> to keep the API flexible without sacrificing type safety. This applies to all icon components in the file, not just the flagged lines (20+ occurrences exist).

The Zod suggestion in the original guideline applies to API boundaries (external APIs, data integration), not component props—the type alias approach above is the appropriate solution here.

Example refactor
+type IconProps = React.SVGProps<SVGSVGElement>
+
 export const Icons = {
-    DecemberLogo: (props: any) => (
+    DecemberLogo: (props: IconProps) => (
         <svg
             {...props}
             viewBox="5 4 14 16"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
DecemberLogo: (props: any) => (
type IconProps = React.SVGProps<SVGSVGElement>
export const Icons = {
DecemberLogo: (props: IconProps) => (
<svg
{...props}
viewBox="5 4 14 16"

Comment on lines +24 to +32
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (selectorRef.current && !selectorRef.current.contains(e.target as Node)) {
setIsModelSelectorOpen(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Outside-click listener is always attached.

handleClickOutside is bound at mount and only unbound on unmount, so it runs on every document mousedown even when the dropdown is closed (most of the time). Gate the effect on isModelSelectorOpen so the listener only exists while the menu is open.

Suggested change
-    useEffect(() => {
-        const handleClickOutside = (e: MouseEvent) => {
-            if (selectorRef.current && !selectorRef.current.contains(e.target as Node)) {
-                setIsModelSelectorOpen(false)
-            }
-        }
-        document.addEventListener('mousedown', handleClickOutside)
-        return () => document.removeEventListener('mousedown', handleClickOutside)
-    }, [])
+    useEffect(() => {
+        if (!isModelSelectorOpen) return
+        const handleClickOutside = (e: MouseEvent) => {
+            if (selectorRef.current && !selectorRef.current.contains(e.target as Node)) {
+                setIsModelSelectorOpen(false)
+            }
+        }
+        document.addEventListener('mousedown', handleClickOutside)
+        return () => document.removeEventListener('mousedown', handleClickOutside)
+    }, [isModelSelectorOpen])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (selectorRef.current && !selectorRef.current.contains(e.target as Node)) {
setIsModelSelectorOpen(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
useEffect(() => {
if (!isModelSelectorOpen) return
const handleClickOutside = (e: MouseEvent) => {
if (selectorRef.current && !selectorRef.current.contains(e.target as Node)) {
setIsModelSelectorOpen(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [isModelSelectorOpen])

Comment on lines +34 to +68
const models = [
{ id: 'Auto', name: 'Auto', desc: 'Best model for your task', icon: null },
{
id: 'Claude 4.7 Opus',
name: 'Claude 4.7 Opus',
desc: "Anthropic's Most Capable Model",
icon: Icons.Claude,
},
{
id: 'Claude 4.6 Sonnet',
name: 'Claude 4.6 Sonnet',
desc: "Anthropic's Latest Model",
icon: Icons.Claude,
},
{ id: 'GPT 5.5', name: 'GPT 5.5', desc: "OpenAI's Latest Model", icon: Icons.OpenAI },
{
id: 'GPT 5.4 - 1M',
name: 'GPT 5.4 - 1M',
desc: '1 Million Context',
icon: Icons.OpenAI,
badge: 'Pro',
},
{
id: 'Gemini 1.5 Pro',
name: 'Gemini 1.5 Pro',
desc: "Google's Latest Model",
icon: Icons.Gemini,
},
{
id: 'DeepSeek V3',
name: 'DeepSeek V3',
desc: 'Powerful Open Source',
icon: Icons.Deepseek,
},
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Hoist models out of the render function.

The array is recreated on every render but never depends on props or state. Move it to module scope (or wrap in useMemo) so referential equality is stable and the component body stays lean. Also consider sourcing it from a config/API later — the UI already has desc and badge fields that are declared but unused, so the data shape is drifting from what the dropdown actually displays.

Suggested move
-export const PromptFooter: React.FC<PromptFooterProps> = ({
+const MODELS = [
+    { id: 'Auto', name: 'Auto', desc: 'Best model for your task', icon: null },
+    // ...rest
+] as const
+
+export const PromptFooter: React.FC<PromptFooterProps> = ({

Comment on lines +83 to +129
<div className="relative" ref={selectorRef}>
<button
onClick={() => setIsModelSelectorOpen(!isModelSelectorOpen)}
className={cn(
'flex items-center gap-2 px-2.5 py-1 rounded-full transition-colors outline-none bg-[#2B2A29]',
isModelSelectorOpen ? 'text-white' : 'text-[#8F8E8D] hover:text-white'
)}
>
<span className="text-[12px] font-medium">{selectedModelData.name}</span>
<Icons.ChevronDown
className={cn(
'w-[11px] h-[11px] transition-transform',
isModelSelectorOpen ? 'rotate-180' : 'rotate-0'
)}
/>
</button>

{isModelSelectorOpen && (
<div className="absolute top-[calc(100%+8px)] left-0 w-[240px] bg-[#1E1D1C] border border-white/[0.08] rounded-xl p-1 shadow-2xl z-50 flex flex-col gap-[2px]">
{models.map((model) => {
const isSelected = selectedModel === model.id
return (
<button
key={model.id}
onClick={() => {
setSelectedModel(model.id)
setIsModelSelectorOpen(false)
}}
className={cn(
'flex items-center justify-between px-3 py-1.5 rounded-lg text-left transition-colors outline-none',
isSelected
? 'bg-[#252422] text-[#D6D5D4]'
: 'text-[#8F8E8D] hover:bg-[#252422] hover:text-[#D6D5D4]'
)}
>
<span className="text-[13px] font-medium truncate">
{model.name}
</span>
{isSelected && (
<Icons.Check className="w-4 h-4 text-[#D6D5D4] shrink-0" />
)}
</button>
)
})}
</div>
)}
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Model dropdown is missing combobox/listbox semantics and keyboard support.

The trigger has no aria-haspopup/aria-expanded, the option buttons have no role="option"/listbox container, there is no Up/Down arrow navigation, no Home/End, and Escape does not close the menu. Keyboard and screen-reader users effectively cannot use the picker. Either adopt a headless library (Radix Select, Headless UI Listbox) or add the ARIA roles and key handlers manually.

@phasehumans phasehumans merged commit f3dbad0 into main May 6, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant