-
-
-
+ {!isEmbedded && (
+
+
+
+ )}
{inlineApp && (
diff --git a/components/email/email-list-item.tsx b/components/email/email-list-item.tsx
index 6ea316b0..2fdd4172 100644
--- a/components/email/email-list-item.tsx
+++ b/components/email/email-list-item.tsx
@@ -21,6 +21,7 @@ interface EmailListItemProps {
email: Email;
selected?: boolean;
onClick?: () => void;
+ onDoubleClick?: () => void;
onContextMenu?: (e: React.MouseEvent, email: Email) => void;
onToggleStar?: () => void;
onMarkAsRead?: (read: boolean) => void;
@@ -30,7 +31,7 @@ interface EmailListItemProps {
onMarkAsSpam?: () => void;
}
-export function EmailListItem({ email, selected, onClick, onContextMenu, onToggleStar, onMarkAsRead, onDelete, onArchive, onSetColorTag, onMarkAsSpam }: EmailListItemProps) {
+export function EmailListItem({ email, selected, onClick, onDoubleClick, onContextMenu, onToggleStar, onMarkAsRead, onDelete, onArchive, onSetColorTag, onMarkAsSpam }: EmailListItemProps) {
const t = useTranslations('email_viewer');
const { selectedEmailIds, toggleEmailSelection, selectRangeEmails, selectedMailbox, mailboxes, clearSelection } = useEmailStore();
const showPreview = useSettingsStore((state) => state.showPreview);
@@ -125,6 +126,12 @@ export function EmailListItem({ email, selected, onClick, onContextMenu, onToggl
onClick?.();
}
}}
+ onDoubleClick={(e) => {
+ if (e.ctrlKey || e.metaKey || e.shiftKey) return;
+ if (!onDoubleClick) return;
+ e.preventDefault();
+ onDoubleClick();
+ }}
onContextMenu={handleContextMenu}
style={{ minHeight: isFocusedMailLayout ? undefined : 'var(--list-item-height)' }}
>
diff --git a/components/email/email-list.tsx b/components/email/email-list.tsx
index 5c39f172..17f11b3d 100644
--- a/components/email/email-list.tsx
+++ b/components/email/email-list.tsx
@@ -23,6 +23,7 @@ interface EmailListProps {
emails: Email[];
selectedEmailId?: string;
onEmailSelect?: (email: Email) => void;
+ onEmailDoubleClick?: (email: Email) => void;
className?: string;
isLoading?: boolean;
onOpenConversation?: (thread: ThreadGroup) => void;
@@ -44,6 +45,7 @@ export function EmailList({
emails,
selectedEmailId,
onEmailSelect,
+ onEmailDoubleClick,
className,
isLoading = false,
onOpenConversation,
@@ -447,6 +449,7 @@ export function EmailList({
expandedEmails={threadEmailsCache.get(thread.threadId)}
onToggleExpand={() => handleToggleThreadExpansion(thread.threadId)}
onEmailSelect={(email) => onEmailSelect?.(email)}
+ onEmailDoubleClick={onEmailDoubleClick ? (email) => onEmailDoubleClick(email) : undefined}
onContextMenu={openContextMenu}
onOpenConversation={onOpenConversation}
onToggleStar={onToggleStar ? (email) => onToggleStar(email) : undefined}
diff --git a/components/email/thread-email-item.tsx b/components/email/thread-email-item.tsx
index bcc25b46..11cfd637 100644
--- a/components/email/thread-email-item.tsx
+++ b/components/email/thread-email-item.tsx
@@ -18,6 +18,7 @@ interface ThreadEmailItemProps {
selected?: boolean;
isLast?: boolean;
onClick?: () => void;
+ onDoubleClick?: () => void;
onContextMenu?: (e: React.MouseEvent, email: Email) => void;
}
@@ -26,6 +27,7 @@ export function ThreadEmailItem({
selected,
isLast = false,
onClick,
+ onDoubleClick,
onContextMenu,
}: ThreadEmailItemProps) {
const t = useTranslations('email_viewer');
@@ -96,6 +98,12 @@ export function ThreadEmailItem({
isPressed && "bg-muted scale-[0.98] ring-2 ring-primary/30"
)}
onClick={handleClick}
+ onDoubleClick={(e) => {
+ if (e.ctrlKey || e.metaKey || e.shiftKey) return;
+ if (!onDoubleClick) return;
+ e.preventDefault();
+ onDoubleClick();
+ }}
onContextMenu={handleContextMenu}
style={{ paddingBlock: 'var(--density-item-py)' }}
>
diff --git a/components/email/thread-list-item.tsx b/components/email/thread-list-item.tsx
index 34f67d5c..ca940644 100644
--- a/components/email/thread-list-item.tsx
+++ b/components/email/thread-list-item.tsx
@@ -25,6 +25,7 @@ interface ThreadListItemProps {
expandedEmails?: Email[];
onToggleExpand: () => void;
onEmailSelect: (email: Email) => void;
+ onEmailDoubleClick?: (email: Email) => void;
onContextMenu?: (e: React.MouseEvent, email: Email) => void;
onOpenConversation?: (thread: ThreadGroup) => void;
onToggleStar?: (email: Email) => void;
@@ -39,6 +40,7 @@ interface SingleEmailItemProps {
email: Email;
selected: boolean;
onClick: () => void;
+ onDoubleClick?: () => void;
onContextMenu?: (e: React.MouseEvent, email: Email) => void;
showPreview: boolean;
colorTag: string | null;
@@ -51,7 +53,7 @@ interface SingleEmailItemProps {
}
const SingleEmailItem = React.forwardRef
(
- function SingleEmailItem({ email, selected, onClick, onContextMenu, showPreview, colorTag, onToggleStar, onMarkAsRead, onDelete, onArchive, onSetColorTag, onMarkAsSpam }, ref) {
+ function SingleEmailItem({ email, selected, onClick, onDoubleClick, onContextMenu, showPreview, colorTag, onToggleStar, onMarkAsRead, onDelete, onArchive, onSetColorTag, onMarkAsSpam }, ref) {
const t = useTranslations('email_viewer');
const isUnread = !email.keywords?.$seen;
const isStarred = email.keywords?.$flagged;
@@ -146,6 +148,12 @@ const SingleEmailItem = React.forwardRef(
isPressed && "bg-muted scale-[0.98] ring-2 ring-primary/30"
)}
onClick={handleClick}
+ onDoubleClick={(e) => {
+ if (e.ctrlKey || e.metaKey || e.shiftKey) return;
+ if (!onDoubleClick) return;
+ e.preventDefault();
+ onDoubleClick();
+ }}
onContextMenu={handleContextMenu}
style={{ minHeight: isFocusedMailLayout ? undefined : 'var(--list-item-height)' }}
>
@@ -351,6 +359,7 @@ export const ThreadListItem = React.forwardRef onEmailSelect(latestEmail)}
+ onDoubleClick={onEmailDoubleClick ? () => onEmailDoubleClick(latestEmail) : undefined}
onContextMenu={onContextMenu}
showPreview={showPreview}
colorTag={colorTag}
@@ -506,6 +516,12 @@ export const ThreadListItem = React.forwardRef {
+ if (e.ctrlKey || e.metaKey || e.shiftKey) return;
+ if (!onEmailDoubleClick) return;
+ e.preventDefault();
+ onEmailDoubleClick(latestEmail);
+ }}
onContextMenu={handleContextMenu}
style={{ minHeight: isFocusedMailLayout ? undefined : 'var(--list-item-height)' }}
>
@@ -762,6 +778,7 @@ export const ThreadListItem = React.forwardRef onEmailSelect(email)}
+ onDoubleClick={onEmailDoubleClick ? () => onEmailDoubleClick(email) : undefined}
onContextMenu={onContextMenu}
/>
))
diff --git a/components/layout/navigation-rail.tsx b/components/layout/navigation-rail.tsx
index d7a08cf1..1d1b05e2 100644
--- a/components/layout/navigation-rail.tsx
+++ b/components/layout/navigation-rail.tsx
@@ -45,6 +45,19 @@ interface NavigationRailProps {
onInlineApp?: (appId: string, url: string, name: string) => void;
onCloseInlineApp?: () => void;
activeAppId?: string | null;
+ /**
+ * If provided, intercepts the rail's built-in route navigation. Return
+ * `true` to prevent the underlying `` from navigating — used by the
+ * Pro interface to open the route as a tab instead. The visual rail is
+ * unchanged.
+ */
+ onNavigate?: (itemId: 'mail' | 'calendar' | 'contacts' | 'files' | 'settings') => boolean | void;
+ /**
+ * When `onNavigate` is in use, this controls which nav item the rail
+ * highlights as active (since the URL alone no longer reflects the
+ * active app).
+ */
+ activeItemId?: 'mail' | 'calendar' | 'contacts' | 'files' | 'settings' | null;
}
function StorageQuotaCircle({ quota, usagePercent }: { quota: { used: number; total: number }; usagePercent: number }) {
@@ -160,6 +173,8 @@ export function NavigationRail({
onInlineApp,
onCloseInlineApp,
activeAppId,
+ onNavigate,
+ activeItemId,
}: NavigationRailProps) {
const t = useTranslations("sidebar");
const pathname = usePathname();
@@ -259,18 +274,39 @@ export function NavigationRail({
{ id: "files", icon: HardDrive, labelKey: "files", href: "/files", hidden: !supportsFiles || !filesEnabled },
];
- const isSettingsActive = !activeAppId && pathname.startsWith("/settings");
+ // When the host (e.g. the Pro shell) takes over navigation via `onNavigate`,
+ // it tells us which item is active; otherwise we infer it from the URL.
+ const isSettingsActive = onNavigate
+ ? activeItemId === 'settings'
+ : !activeAppId && pathname.startsWith("/settings");
const visibleItems = navItems.filter((item) => !item.hidden);
- const getIsActive = (href: string) => {
+ const getIsActive = (href: string, itemId: string) => {
if (activeAppId) return false;
+ if (onNavigate) {
+ return activeItemId === itemId;
+ }
if (href === "/") {
return pathname === "/" || pathname === "";
}
return pathname.startsWith(href);
};
+ const handleNavClick = (itemId: 'mail' | 'calendar' | 'contacts' | 'files' | 'settings') =>
+ (e: React.MouseEvent) => {
+ if (onNavigate) {
+ const intercepted = onNavigate(itemId);
+ if (intercepted !== false) {
+ e.preventDefault();
+ }
+ return;
+ }
+ if (activeAppId) {
+ onCloseInlineApp?.();
+ }
+ };
+
if (orientation === "horizontal") {
return (