Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,15 @@
flex-shrink: 0;
}

/* Ensure flex-1 wrappers inside icons bar don't expand too much */
.mobile-icons-bar-content .flex-1 {
flex: 1 1 0;
min-width: 0;
display: flex;
justify-content: center;
align-items: center;
}

/*
.mobile-chat-section {
flex: 1;
Expand Down
2 changes: 1 addition & 1 deletion components/header-search-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ export function HeaderSearchButton() {
)

const mobileButton = (
<Button variant="ghost" size="icon" onClick={handleResolutionSearch} disabled={isAnalyzing || !map || !actions} data-testid="mobile-search-button">
<Button variant="ghost" size="icon" onClick={handleResolutionSearch} disabled={isAnalyzing || !actions || (mapProvider === 'mapbox' && !map)} data-testid="mobile-search-button">
{isAnalyzing ? (
<div className="h-[1.2rem] w-[1.2rem] animate-spin rounded-full border-b-2 border-current"></div>
) : (
Expand Down
4 changes: 2 additions & 2 deletions components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import React from 'react'
import Image from 'next/image'
import { useCalendarToggle } from './calendar-toggle-context'
import { ModeToggle } from './mode-toggle'
import { History } from './history'
import { cn } from '@/lib/utils'
import HistoryContainer from './history-container'
import { Button } from '@/components/ui/button'
Expand Down Expand Up @@ -81,7 +81,7 @@ export const Header = () => {
<TentTree className="h-[1.2rem] w-[1.2rem]" />
</Button>

<ModeToggle />
<History location="header" />

<HistoryContainer location="header" />
</div>
Expand Down
12 changes: 7 additions & 5 deletions components/mobile-icons-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
} from 'lucide-react'
import { History } from '@/components/history'
import { MapToggle } from './map-toggle'
import { ModeToggle } from './mode-toggle'
import { ProfileToggle } from './profile-toggle'
import { useCalendarToggle } from './calendar-toggle-context'
import { useUsageToggle } from './usage-toggle-context'
Expand Down Expand Up @@ -48,12 +47,16 @@ export const MobileIconsBar: React.FC<MobileIconsBarProps> = ({ onAttachmentClic
}

return (
<div className="mobile-icons-bar-content">
<div className="mobile-icons-bar-content flex items-center justify-around w-full">
<Button variant="ghost" size="icon" onClick={handleNewChat} data-testid="mobile-new-chat-button">
<Plus className="h-[1.2rem] w-[1.2rem]" />
</Button>
<ProfileToggle />
<MapToggle />
<div className="flex-1 flex justify-center">
<ProfileToggle />
</div>
<div className="flex-1 flex justify-center">
<MapToggle />
</div>
<Button variant="ghost" size="icon" onClick={toggleCalendar} title="Open Calendar" data-testid="mobile-calendar-button">
<CalendarDays className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
</Button>
Expand All @@ -68,7 +71,6 @@ export const MobileIconsBar: React.FC<MobileIconsBarProps> = ({ onAttachmentClic
<ArrowRight className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
</Button>
<History location="header" />
<ModeToggle />
</div>
)
}
Expand Down
9 changes: 8 additions & 1 deletion components/profile-toggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,16 @@ export function ProfileToggle() {
return <CircleUserRound className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
}

// Mobile: show profile icon as a direct button that opens the profile view
if (isMobile) {
return (
<Button variant="ghost" size="icon" className="relative" data-testid="profile-toggle" disabled>
<Button
variant="ghost"
size="icon"
className="relative"
data-testid="profile-toggle"
onClick={() => handleSectionToggle(ProfileToggleEnum.Settings)}
>
<ProfileIcon />
<span className="sr-only">Open profile menu</span>
</Button>
Expand Down
52 changes: 50 additions & 2 deletions components/settings/components/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import { useRouter } from 'next/navigation'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import * as z from 'zod'
import { Loader2, Save, RotateCcw } from 'lucide-react'
import { Loader2, Save, RotateCcw, Sun, Moon, Earth } from 'lucide-react'
import { useTheme } from 'next-themes'
import { cn } from '@/lib/utils'

import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
Expand Down Expand Up @@ -71,6 +73,12 @@ export function Settings({ initialTab = "system-prompt" }: SettingsProps) {
const [currentTab, setCurrentTab] = useState(initialTab);
const { mapProvider, setMapProvider } = useSettingsStore();
const { user, loading: authLoading } = useCurrentUser();
const { theme, setTheme } = useTheme()
const [mounted, setMounted] = useState(false)

useEffect(() => {
setMounted(true)
}, [])

useEffect(() => {
setCurrentTab(initialTab);
Expand Down Expand Up @@ -153,6 +161,7 @@ export function Settings({ initialTab = "system-prompt" }: SettingsProps) {

function onReset() {
form.reset(defaultValues)
setTheme('earth')
toast({
title: "Settings reset",
description: "Your settings have been reset to default values.",
Expand All @@ -163,8 +172,45 @@ export function Settings({ initialTab = "system-prompt" }: SettingsProps) {
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="space-y-6">
{/* Theme selector placed above the tabs */}
<Card className="mb-6">
<CardHeader>
<CardTitle>Theme Selection</CardTitle>
<CardDescription>Select the theme for your planetary copilot</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-3 gap-4">
{[
{ name: 'Light', value: 'light', icon: Sun },
{ name: 'Dark', value: 'dark', icon: Moon },
{ name: 'Earth', value: 'earth', icon: Earth },
].map((t) => {
const Icon = t.icon
const isActive = mounted && theme === t.value
return (
<button
key={t.value}
type="button"
data-testid={`theme-select-${t.value}`}
onClick={() => setTheme(t.value)}
className={cn(
"flex flex-col items-center justify-center p-6 rounded-xl border-2 transition-all gap-3 text-center",
isActive
? "bg-accent/40 border-primary text-primary shadow-sm"
: "bg-card border-muted hover:border-muted-foreground/50 text-muted-foreground hover:text-foreground"
)}
>
<Icon className="h-8 w-8" />
<span className="text-sm font-medium">{t.name}</span>
</button>
)
})}
</div>
</CardContent>
</Card>

<Tabs.Root value={currentTab} onValueChange={setCurrentTab} className="w-full">
<Tabs.List className="grid w-full grid-cols-4 gap-x-2">
<Tabs.List className="grid w-full grid-cols-2 md:grid-cols-4 gap-2">
<Tabs.Trigger value="system-prompt" className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 data-[state=active]:bg-primary/80">System Prompt</Tabs.Trigger>
<Tabs.Trigger value="model" className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 data-[state=active]:bg-primary/80">Model Selection</Tabs.Trigger>
<Tabs.Trigger value="user-management" className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 data-[state=active]:bg-primary/80">User Management</Tabs.Trigger>
Expand Down Expand Up @@ -229,6 +275,8 @@ export function Settings({ initialTab = "system-prompt" }: SettingsProps) {
</CardContent>
</Card>
</Tabs.Content>


</motion.div>
</AnimatePresence>
</Tabs.Root>
Expand Down
1 change: 1 addition & 0 deletions components/settings/settings-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { DownloadReportButton } from "@/components/download-report-button"

export default function SettingsView() {
const { toggleProfileSection, activeView } = useProfileToggle();
// Appearance now shows the theme selector (above tabs) with system-prompt as default tab
const initialTab = activeView === ProfileToggleEnum.Security ? "user-management" : "system-prompt";

const handleClose = () => {
Expand Down
15 changes: 10 additions & 5 deletions tests/header.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,21 @@ import { test, expect } from '@playwright/test';
test.describe('Header and Navigation', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
try {
await page.locator('text=Later').click({ timeout: 3000 });
} catch (e) {}
});
Comment on lines +6 to 9

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the Later dismissal into a shared test helper.

The same try/catch block to dismiss the optional text=Later prompt is duplicated across 6 beforeEach hooks (1 in this file, 5 in tests/responsive.spec.ts). Extract it into a shared helper to reduce duplication and make future changes easier.

♻️ Proposed helper extraction

Create a shared helper, e.g. in tests/utils.ts:

export async function dismissOptionalLaterPrompt(page: import('`@playwright/test`').Page) {
  try {
    await page.locator('text=Later').click({ timeout: 3000 });
  } catch {}
}

Then use it in each beforeEach:

 test.beforeEach(async ({ page }) => {
   await page.goto('/');
-  try {
-    await page.locator('text=Later').click({ timeout: 3000 });
-  } catch (e) {}
+  await dismissOptionalLaterPrompt(page);
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/header.spec.ts` around lines 6 - 9, Extract the duplicated optional
“Later” prompt dismissal into a shared helper such as dismissOptionalLaterPrompt
in tests/utils.ts, preserving the existing locator, timeout, and swallowed-error
behavior. Replace the inline try/catch blocks in the beforeEach hooks of
header.spec.ts and responsive.spec.ts with calls to this helper.


test('should toggle the theme', async ({ page }) => {
await page.click('[data-testid="theme-toggle"]');
await page.click('[data-testid="theme-dark"]');
test('should toggle the theme in settings', async ({ page }) => {
// Theme selector is now placed above the settings tabs
await page.click('[data-testid="profile-toggle"]');
await page.click('[data-testid="profile-settings"]');

await page.click('[data-testid="theme-select-dark"]');
const html = page.locator('html');
await expect(html).toHaveClass(/(^|\s)dark(\s|$)/);

await page.click('[data-testid="theme-toggle"]');
await page.click('[data-testid="theme-light"]');
await page.click('[data-testid="theme-select-light"]');
await expect(html).not.toHaveClass(/(^|\s)dark(\s|$)/);
});

Expand Down
6 changes: 3 additions & 3 deletions tests/mobile.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ test.describe('Mobile UI', () => {
await page.locator('[data-testid="chat-input"]').waitFor({ state: 'visible', timeout: 30000 });
});

test('profile toggle button should be disabled', async ({ page }) => {
// Check that the profile toggle is disabled on mobile
await expect(page.locator('.mobile-icons-bar-content [data-testid="profile-toggle"]')).toBeDisabled();
test('profile toggle button should be enabled on mobile', async ({ page }) => {
// Check that the profile toggle is enabled and clickable on mobile
await expect(page.locator('.mobile-icons-bar-content [data-testid="profile-toggle"]')).toBeEnabled();
});

test('should have an enabled submit button', async ({ page }) => {
Expand Down
17 changes: 16 additions & 1 deletion tests/responsive.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ test.describe('Responsive design - Desktop', () => {

test.beforeEach(async ({ page }) => {
await page.goto('/');
try {
await page.locator('text=Later').click({ timeout: 3000 });
} catch (e) {}
});

test('should display desktop layout', async ({ page }) => {
Expand All @@ -18,7 +21,7 @@ test.describe('Responsive design - Desktop', () => {
});

test('should display full header with all elements', async ({ page }) => {
await expect(page.locator('[data-testid="theme-toggle"]')).toBeVisible();
await expect(page.locator('[data-testid="history-button"]').first()).toBeVisible();
await expect(page.locator('[data-testid="profile-toggle"]')).toBeVisible();
await expect(page.locator('[data-testid="calendar-toggle"]')).toBeVisible();
});
Expand All @@ -44,6 +47,9 @@ test.describe('Responsive design - Tablet', () => {

test.beforeEach(async ({ page }) => {
await page.goto('/');
try {
await page.locator('text=Later').click({ timeout: 3000 });
} catch (e) {}
});

test('should display tablet layout', async ({ page }) => {
Expand Down Expand Up @@ -96,6 +102,9 @@ test.describe('Responsive design - Mobile', () => {

test.beforeEach(async ({ page }) => {
await page.goto('/');
try {
await page.locator('text=Later').click({ timeout: 3000 });
} catch (e) {}
});

test('should display mobile layout', async ({ page }) => {
Expand Down Expand Up @@ -195,6 +204,9 @@ test.describe('Responsive design - Small Mobile', () => {

test.beforeEach(async ({ page }) => {
await page.goto('/');
try {
await page.locator('text=Later').click({ timeout: 3000 });
} catch (e) {}
});

test('should work on very small screens', async ({ page }) => {
Expand Down Expand Up @@ -229,6 +241,9 @@ test.describe('Responsive design - Large Desktop', () => {

test.beforeEach(async ({ page }) => {
await page.goto('/');
try {
await page.locator('text=Later').click({ timeout: 3000 });
} catch (e) {}
});

test('should utilize large screen space', async ({ page }) => {
Expand Down