+ compact ? (
+ // Mirrors the zoom buttons on the left: same inset, same 70px height.
+
diff --git a/frontend/src/components/map/PinShape/PinShape.tsx b/frontend/src/components/map/PinShape/PinShape.tsx
index 86fa39d..9652804 100644
--- a/frontend/src/components/map/PinShape/PinShape.tsx
+++ b/frontend/src/components/map/PinShape/PinShape.tsx
@@ -1,7 +1,6 @@
import { useId, type ReactNode } from 'react';
+import { TRIAL_STATUS, type TrialStatus } from '@/constants/trialStatus';
import { cn } from '@/lib/utils';
-import { TRIAL_STATUS } from '@/lib/trialStatus';
-import type { TrialStatus } from '@/types/trial';
const PIN_PATH =
'M16 3C10.477 3 6 7.477 6 13c0 7.25 10 18 10 18s10-10.75 10-18c0-5.523-4.477-10-10-10z';
diff --git a/frontend/src/components/map/TrialCluster/TrialCluster.tsx b/frontend/src/components/map/TrialCluster/TrialCluster.tsx
index 6ca847e..39a1b3f 100644
--- a/frontend/src/components/map/TrialCluster/TrialCluster.tsx
+++ b/frontend/src/components/map/TrialCluster/TrialCluster.tsx
@@ -4,22 +4,13 @@ import { Button } from '@/components/ui/button';
import { Command, CommandGroup, CommandItem, CommandList } from '@/components/ui/command';
import CountPin from '@/components/map/CountPin/CountPin';
import DualPin, { type StatusCount } from '@/components/map/DualPin/DualPin';
-import { TRIAL_STATUS } from '@/lib/trialStatus';
-import { cn } from '@/lib/utils';
-import type { TrialStatus } from '@/types/trial';
-
-export interface ClusterItem {
- trialRef: string | null;
- title: string;
- status: TrialStatus;
-}
+import StatusDot from '@/components/trial/StatusDot/StatusDot';
+import { trialTitle } from '@/lib/trial';
+import type { PinUnit } from '@/types/map';
interface TrialClusterProps {
- longitude: number;
- latitude: number;
- locationName: string;
- items: ClusterItem[];
- selectedTrialRef?: string | null;
+ unit: PinUnit;
+ selectedTrialRef: string | null;
open: boolean;
onToggle: () => void;
onClose: () => void;
@@ -27,10 +18,7 @@ interface TrialClusterProps {
}
function TrialCluster({
- longitude,
- latitude,
- locationName,
- items,
+ unit,
selectedTrialRef,
open,
onToggle,
@@ -38,8 +26,9 @@ function TrialCluster({
onSelectTrial,
}: TrialClusterProps) {
const { t } = useTranslation();
- const selected = items.some((item) => item.trialRef === selectedTrialRef);
- const selectedStatus = items.find((item) => item.trialRef === selectedTrialRef)?.status ?? null;
+ const { longitude, latitude, locationName, items } = unit;
+ const selectedStatus =
+ items.find((item) => item.trial.trialRef === selectedTrialRef)?.status ?? null;
const groups = items.reduce((acc, item) => {
const group = acc.find((entry) => entry.status === item.status);
return group
@@ -63,7 +52,11 @@ function TrialCluster({
{groups.length > 1 ? (
) : (
-
+
)}
@@ -90,27 +83,18 @@ function TrialCluster({
- {items.map((item, index) => (
+ {items.map(({ trial, status }) => (
{
- if (item.trialRef) {
- onSelectTrial(item.trialRef);
- onClose();
- }
+ onSelectTrial(trial.trialRef);
+ onClose();
}}
>
-
- {item.title}
+
+ {trialTitle(trial)}
))}
diff --git a/frontend/src/components/map/TrialMarker/TrialMarker.tsx b/frontend/src/components/map/TrialMarker/TrialMarker.tsx
deleted file mode 100644
index 4c180ed..0000000
--- a/frontend/src/components/map/TrialMarker/TrialMarker.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import { Marker } from 'react-map-gl/mapbox';
-import { Button } from '@/components/ui/button';
-import CountPin from '@/components/map/CountPin/CountPin';
-import type { TrialStatus } from '@/types/trial';
-
-interface TrialMarkerProps {
- longitude: number;
- latitude: number;
- status: TrialStatus;
- selected: boolean;
- label: string;
- onSelect: () => void;
-}
-
-function TrialMarker({ longitude, latitude, status, selected, label, onSelect }: TrialMarkerProps) {
- return (
-
-
-
- );
-}
-
-export default TrialMarker;
diff --git a/frontend/src/components/mobile/MobileLayout/MobileLayout.tsx b/frontend/src/components/mobile/MobileLayout/MobileLayout.tsx
index d18ee0f..faf5280 100644
--- a/frontend/src/components/mobile/MobileLayout/MobileLayout.tsx
+++ b/frontend/src/components/mobile/MobileLayout/MobileLayout.tsx
@@ -11,60 +11,28 @@ import { MOBILE_LAYOUT, sheetHeights } from '@/constants/layout';
import { useViewportHeight } from '@/hooks/useIsMobile';
import { cn } from '@/lib/utils';
import { useAppStore } from '@/store/appStore';
-import type { Trial } from '@/types/trial';
const MAP_TUCK_PX = 16;
const CAROUSEL_GAP_PX = 12;
-// No coverage note on mobile; the legend mirrors the zoom buttons (10px inset, 70px tall) on the left.
-const MAP_OVERLAYS =
- '[&_[data-slot=map-info]]:hidden [&_[data-slot=map-legend]]:bottom-auto [&_[data-slot=map-legend]]:left-[10px] [&_[data-slot=map-legend]]:flex [&_[data-slot=map-legend]]:h-[70px] [&_[data-slot=map-legend]]:items-center [&_[data-slot=map-legend]]:py-0';
-const MAP_OVERLAYS_BESIDE_SHEET = '[&_[data-slot=map-legend]]:top-[10px]';
-// Behind the search bar both groups drop below it; `!` beats Mapbox's unlayered CSS.
-const MAP_OVERLAYS_UNDER_SEARCH =
- '[&_.mapboxgl-ctrl-top-right]:top-19! [&_[data-slot=map-legend]]:top-[calc(var(--spacing)*19+10px)]';
+// The search bar's footprint at the top of a full-height map (its h-14 plus the 0.75rem inset and a gap).
+const SEARCH_BAR_CLEARANCE_PX = 76;
interface MobileLayoutProps {
- dark: boolean;
- trials: Trial[];
- selectedTrial: Trial | null;
- selectedSiteKey: string | null;
- selectedSiteName: string | null;
- selectedInContext: boolean;
- selectedIsBookmarked: boolean;
- contextTrials: Trial[];
- bookmarkCount: number;
- onRemoveContext: (trialRef: string) => void;
onOpenBookmarks: () => void;
onStartTour: () => void;
children: ReactNode;
}
-function MobileLayout({
- dark,
- trials,
- selectedTrial,
- selectedSiteKey,
- selectedSiteName,
- selectedInContext,
- selectedIsBookmarked,
- contextTrials,
- bookmarkCount,
- onRemoveContext,
- onOpenBookmarks,
- onStartTour,
- children,
-}: MobileLayoutProps) {
+function MobileLayout({ onOpenBookmarks, onStartTour, children }: MobileLayoutProps) {
const { t } = useTranslation();
- const conversationTitle = useAppStore((state) => state.conversationTitle);
- const { setTrials, selectTrial, reset, addToContext, toggleBookmark, toggleTheme } =
- useAppStore.getState();
-
+ const hasTrials = useAppStore((state) => state.trials.length > 0);
+ const hasSelection = useAppStore((state) => state.selectedTrialRef !== null);
+ const selectTrial = useAppStore((state) => state.selectTrial);
const snap = useAppStore((state) => state.sheetSnap);
- const setSnap = useAppStore.getState().setSheetSnap;
+ const setSnap = useAppStore((state) => state.setSheetSnap);
const detailOpen = useAppStore((state) => state.trialDrawerOpen);
- const setDetailOpen = useAppStore.getState().setTrialDrawerOpen;
+ const setDetailOpen = useAppStore((state) => state.setTrialDrawerOpen);
const heights = sheetHeights(useViewportHeight());
- const selectedTrialRef = selectedTrial?.trialRef ?? null;
// A cited trial collapses the chat so its card is one tap from the details.
const showTrial = (trialRef: string) => {
@@ -72,8 +40,7 @@ function MobileLayout({
setSnap('peek');
};
- const askAbout = (trialRef: string) => {
- addToContext(trialRef);
+ const returnToChat = () => {
setDetailOpen(false);
setSnap('full');
};
@@ -82,35 +49,20 @@ function MobileLayout({
{/* Beside a half sheet the map fits between the search bar and the sheet. */}
-
+
setSnap('full')}
onOpenBookmarks={onOpenBookmarks}
onStartTour={onStartTour}
- onToggleTheme={toggleTheme}
/>
- setDetailOpen(true)}
- />
+ setDetailOpen(true)} />
0 ? t('mobile.chatPeek') : t('mobile.searchPrompt')}
+ peekLabel={hasTrials ? t('mobile.chatPeek') : t('mobile.searchPrompt')}
onSnapChange={setSnap}
>
-
+
-
+
{t('summary.trialDetails')}
setDetailOpen(false)}
- onAddToContext={askAbout}
- isInContext={selectedInContext}
- onToggleBookmark={toggleBookmark}
- isBookmarked={selectedIsBookmarked}
- selectedSiteName={selectedSiteName}
+ onAddedToContext={returnToChat}
actionBar
/>
diff --git a/frontend/src/components/mobile/SearchBar/SearchBar.tsx b/frontend/src/components/mobile/SearchBar/SearchBar.tsx
index 15838f2..a21b8c0 100644
--- a/frontend/src/components/mobile/SearchBar/SearchBar.tsx
+++ b/frontend/src/components/mobile/SearchBar/SearchBar.tsx
@@ -19,41 +19,34 @@ import {
SheetTitle,
SheetTrigger,
} from '@/components/ui/sheet';
+import LanguageOptions from '@/components/language/LanguageOptions/LanguageOptions';
+import BrandTitle from '@/components/layout/BrandTitle/BrandTitle';
import CamilleIcon from '@/components/layout/CamilleIcon/CamilleIcon';
import CornerMark from '@/components/layout/CornerMark/CornerMark';
-import { LanguageOptions } from '@/components/layout/LanguagePicker/LanguagePicker';
import OicrLogo from '@/components/layout/OicrLogo/OicrLogo';
import { AGENT_NAME } from '@/constants/chat';
import { LANGUAGE_TARGETS } from '@/constants/language';
import { useAppLanguage } from '@/hooks/useAppLanguage';
import { cn } from '@/lib/utils';
+import { useAppStore } from '@/store/appStore';
interface SearchBarProps {
- /** The conversation title; null shows the placeholder prompt. */
- title: string | null;
- dark: boolean;
- bookmarkCount: number;
onOpenChat: () => void;
onOpenBookmarks: () => void;
onStartTour: () => void;
- onToggleTheme: () => void;
}
const MENU_ROW = 'h-11 justify-start gap-3 px-3 text-sm';
const HIGHLIGHT_EDGE =
"after:bg-highlight relative overflow-hidden after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:content-['']";
-function SearchBar({
- title,
- dark,
- bookmarkCount,
- onOpenChat,
- onOpenBookmarks,
- onStartTour,
- onToggleTheme,
-}: SearchBarProps) {
+function SearchBar({ onOpenChat, onOpenBookmarks, onStartTour }: SearchBarProps) {
const { t } = useTranslation();
const { language } = useAppLanguage();
+ const title = useAppStore((state) => state.conversationTitle);
+ const dark = useAppStore((state) => state.theme === 'dark');
+ const toggleTheme = useAppStore((state) => state.toggleTheme);
+ const bookmarkCount = useAppStore((state) => state.bookmarkedTrialRefs.length);
const [languageOpen, setLanguageOpen] = useState(false);
const current = LANGUAGE_TARGETS.find((option) => option.code === language);
@@ -93,19 +86,8 @@ function SearchBar({
-
-
-
- {AGENT_NAME}
-
-
- {t('app.preRelease')}
-
-
-
- {t('app.tagline')}
-
-
+ {AGENT_NAME}
+