diff --git a/src/e2e/src/__tests__/mailbox-settings.spec.ts b/src/e2e/src/__tests__/mailbox-settings.spec.ts
index 8658dfd7..dd385f3c 100644
--- a/src/e2e/src/__tests__/mailbox-settings.spec.ts
+++ b/src/e2e/src/__tests__/mailbox-settings.spec.ts
@@ -71,8 +71,12 @@ test.describe("Mailbox settings modal", () => {
// The user administers two mailboxes (their own + the import mailbox) and is
// only a sender on the shared mailbox, so the switcher is rendered and lists
// exactly the two administered mailboxes.
+ //
+ // The switcher trigger is the shared MailboxSelector card: its accessible
+ // name is the currently-configured mailbox (the user's own administered
+ // mailbox) rather than a fixed label.
await modal
- .getByRole("button", { name: "Select the mailbox to configure" })
+ .getByRole("button", { name: `user.e2e.${browserName}@example.local` })
.click();
// The switcher renders a single-select dropdown, so its entries expose a
diff --git a/src/e2e/src/__tests__/message-import.spec.ts b/src/e2e/src/__tests__/message-import.spec.ts
index ce053fbb..644ca92a 100644
--- a/src/e2e/src/__tests__/message-import.spec.ts
+++ b/src/e2e/src/__tests__/message-import.spec.ts
@@ -23,7 +23,7 @@ test.describe("Import Message", () => {
// Go the import mailbox
await page.getByRole("button", { name: getMailboxEmail('user', browserName) }).click();
- await page.getByRole("menuitem", { name: getMailboxEmail('import') }).click();
+ await page.getByRole("menuitem").filter({ hasText: getMailboxEmail('import') }).click();
await page.waitForLoadState("networkidle");
// As the database is fresh, there should be no threads and the Import messages button should be visible
@@ -117,7 +117,8 @@ test.describe("Import Message", () => {
// Go to the shared mailbox where the user only has sender rights
await page.getByRole("button", { name: email }).click();
await page
- .getByRole("menuitem", { name: getMailboxEmail("shared") })
+ .getByRole("menuitem")
+ .filter({ hasText: getMailboxEmail("shared") })
.click();
await page.waitForLoadState("networkidle");
diff --git a/src/e2e/src/__tests__/message-inline-image.spec.ts b/src/e2e/src/__tests__/message-inline-image.spec.ts
index 97984ec3..5daa5628 100644
--- a/src/e2e/src/__tests__/message-inline-image.spec.ts
+++ b/src/e2e/src/__tests__/message-inline-image.spec.ts
@@ -112,7 +112,7 @@ test.describe("Inline Image in Composer", () => {
// Switch to shared mailbox and verify the message is received
// Message delivery is async (Celery task), so we poll until the message appears
await page.getByTestId('panel-main-left').getByRole("button", { name: getMailboxEmail('user', browserName) }).click();
- await page.getByRole("menuitem", { name: getMailboxEmail('shared') }).click();
+ await page.getByRole("menuitem").filter({ hasText: getMailboxEmail('shared') }).click();
await page.waitForLoadState("networkidle");
await page.getByRole("link", { name: "Inbox" }).click();
diff --git a/src/e2e/src/__tests__/message-send.spec.ts b/src/e2e/src/__tests__/message-send.spec.ts
index 44eb5c25..0a451890 100644
--- a/src/e2e/src/__tests__/message-send.spec.ts
+++ b/src/e2e/src/__tests__/message-send.spec.ts
@@ -59,7 +59,7 @@ test.describe("Send Message", () => {
// Go the shared mailbox and check if the message is there
await page.getByRole("button", { name: getMailboxEmail('user', browserName) }).click();
- await page.getByRole("menuitem", { name: getMailboxEmail('shared') }).click();
+ await page.getByRole("menuitem").filter({ hasText: getMailboxEmail('shared') }).click();
await page.waitForLoadState("networkidle");
await page.getByRole("link", { name: "Inbox" }).click();
diff --git a/src/e2e/src/__tests__/thread-event.spec.ts b/src/e2e/src/__tests__/thread-event.spec.ts
index 153d69f6..d972ac95 100644
--- a/src/e2e/src/__tests__/thread-event.spec.ts
+++ b/src/e2e/src/__tests__/thread-event.spec.ts
@@ -33,7 +33,8 @@ async function navigateToSharedThread(page: Page, browserName: BrowserName) {
.getByRole("button", { name: getMailboxEmail("user", browserName) })
.click();
await page
- .getByRole("menuitem", { name: getMailboxEmail("shared") })
+ .getByRole("menuitem")
+ .filter({ hasText: getMailboxEmail("shared") })
.click();
await page.waitForLoadState("networkidle");
diff --git a/src/e2e/src/utils-test.ts b/src/e2e/src/utils-test.ts
index 3cd71802..0ed36e7a 100644
--- a/src/e2e/src/utils-test.ts
+++ b/src/e2e/src/utils-test.ts
@@ -68,9 +68,17 @@ export const signInKeycloakIfNeeded = async ({ page, username, navigateTo = "/"
await page.click('button[type="submit"]');
await page.waitForURL(`/`, { waitUntil: 'networkidle' });
- expect(proConnectButton).not.toBeVisible();
- const mailboxName = await page.getByRole('button', { name: email });
- expect(mailboxName).toBeVisible();
+ await expect(proConnectButton).not.toBeVisible();
+
+ // Confirm the authenticated app shell rendered before snapshotting storage
+ // state. The sidebar mailbox selector shows the signed-in address, but it
+ // renders as a switcher *button* only for multi-mailbox users; single-mailbox
+ // fixtures (e.g. domain_admin) get a static card instead. Match on the
+ // address text within the selector rather than a button role, which covers
+ // both variants.
+ await expect(
+ page.locator('.mailbox-selector').getByText(email),
+ ).toBeVisible();
await page.context().storageState({ path: storageStatePath });
};
diff --git a/src/frontend/src/features/layouts/components/mailbox-panel/_index.scss b/src/frontend/src/features/layouts/components/mailbox-panel/_index.scss
index 64e29154..b3c40a32 100644
--- a/src/frontend/src/features/layouts/components/mailbox-panel/_index.scss
+++ b/src/frontend/src/features/layouts/components/mailbox-panel/_index.scss
@@ -23,33 +23,11 @@
flex-shrink: 0;
padding-bottom: var(--c--globals--spacings--xs);
- & > .c__dropdown-menu-trigger {
- justify-content: flex-start;
- width: 100%;
- }
-
- button.mailbox-panel__mailbox-title__dropdown-button {
- color: var(--c--contextuals--content--semantic--neutral--primary);
- font-weight: 700;
- padding-block: var(--c--globals--spacings--sm);
+ // The trigger card itself lives in the shared MailboxSelector component; here
+ // we only nudge its trailing padding so the unfold chevron lines up with the
+ // action icons sitting above it in the sidebar header.
+ .mailbox-selector__trigger {
// 1.1rem hardcoded to be aligned with other icon in the sidebar
- padding-inline: var(--c--globals--spacings--base) 1.1rem;
- width: 100%;
- height: inherit;
-
- & > .button__label {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- flex: 1;
- text-align: left;
- justify-content: flex-start;
- }
-
- &:hover,
- &:focus,
- &:active {
- color: inherit;
- }
+ padding-inline-end: 1.1rem;
}
}
diff --git a/src/frontend/src/features/layouts/components/mailbox-panel/index.tsx b/src/frontend/src/features/layouts/components/mailbox-panel/index.tsx
index 8181421e..8dac5629 100644
--- a/src/frontend/src/features/layouts/components/mailbox-panel/index.tsx
+++ b/src/frontend/src/features/layouts/components/mailbox-panel/index.tsx
@@ -1,68 +1,40 @@
-import { DropdownMenu, HorizontalSeparator, Icon, Spinner } from "@gouvfr-lasuite/ui-kit"
-import { ChevronDown, ChevronUp } from "@gouvfr-lasuite/ui-kit/icons";
+import { HorizontalSeparator, Spinner } from "@gouvfr-lasuite/ui-kit"
import { MailboxPanelActions } from "./components/mailbox-actions"
import { MailboxList } from "./components/mailbox-list"
import { useMailboxContext } from "@/features/providers/mailbox";
-import { Button } from "@gouvfr-lasuite/cunningham-react";
import { useNavigate } from "@tanstack/react-router";
import { useUrlSearchParams } from "@/hooks/use-url-search-params";
import { useLayoutContext } from "@/features/layouts/components/layout-context";
import { MailboxLabels } from "./components/mailbox-labels";
-import { useState } from "react";
import { Group, Panel, Separator, useDefaultLayout } from "react-resizable-panels";
-import MailboxHelper from "@/features/utils/mailbox-helper";
+import { MailboxSelector } from "@/features/layouts/components/mailbox-selector";
export const MailboxPanel = () => {
const navigate = useNavigate();
const searchParams = useUrlSearchParams();
const { selectedMailbox, mailboxes, queryStates } = useMailboxContext();
const { closeLeftPanel } = useLayoutContext();
- const [isOpen, setIsOpen] = useState(false);
const { defaultLayout, onLayoutChange } = useDefaultLayout({
groupId: "mailbox-panel-sections",
storage: typeof window !== "undefined" ? localStorage : undefined,
});
- const getMailboxOptions = () => {
- if (!mailboxes) return [];
- const sortedMailboxes = MailboxHelper.sortByKind(mailboxes);
- return sortedMailboxes.map((mailbox, index) => ({
- label: mailbox.email,
- value: mailbox.id,
- icon: mailbox.is_identity ? : ,
- showSeparator: MailboxHelper.showSeparatorAfter(sortedMailboxes, index)
- }));
- }
-
return (
- { selectedMailbox && (
-
- {
- closeLeftPanel();
- navigate({ to: '/mailbox/$mailboxId', params: { mailboxId: value }, search: Object.fromEntries(searchParams) });
- }}
- >
- : }
- iconPosition="right"
- onClick={() => setIsOpen(!isOpen)}
- >
- {selectedMailbox.email}
-
-
-
+ { selectedMailbox && mailboxes && (
+
+ {
+ closeLeftPanel();
+ navigate({ to: '/mailbox/$mailboxId', params: { mailboxId }, search: Object.fromEntries(searchParams) });
+ }}
+ />
+
)}
{!selectedMailbox || queryStates.mailboxes.isLoading ?
:
diff --git a/src/frontend/src/features/layouts/components/mailbox-selector/_index.scss b/src/frontend/src/features/layouts/components/mailbox-selector/_index.scss
new file mode 100644
index 00000000..76239a48
--- /dev/null
+++ b/src/frontend/src/features/layouts/components/mailbox-selector/_index.scss
@@ -0,0 +1,99 @@
+.mailbox-selector {
+ // Make the dropdown trigger wrapper span the full available width so the
+ // card aligns with the surrounding sidebar/header content.
+ & > .c__dropdown-menu-trigger {
+ justify-content: flex-start;
+ width: 100%;
+ }
+
+ // Card shared by the switchable (button) and the static (single-mailbox)
+ // variants: avatar on the left, bold name and the address underneath, with
+ // the unfold chevron pushed to the trailing edge.
+ .mailbox-selector__trigger {
+ display: flex;
+ align-items: center;
+ gap: var(--c--globals--spacings--xs);
+ width: 100%;
+ height: inherit;
+ padding-block: var(--c--globals--spacings--base) var(--c--globals--spacings--sm);
+ padding-inline: var(--c--globals--spacings--base);
+ .c__button__icon {
+ transition: transform 0.25s ease;
+ backface-visibility: visible;
+ }
+ }
+
+ .mailbox-selector__trigger[aria-expanded="true"] {
+ .c__button__icon {
+ transform: rotateX(-180deg);
+ }
+ }
+
+ .mailbox-selector__trigger--static {
+ // Nothing to unfold for a single mailbox: drop the interactive cursor.
+ cursor: default;
+ }
+
+ // Shared (non-identity) mailboxes get a rounded-square avatar to set them
+ // apart from the circular personal one.
+ .mailbox-selector__avatar[data-shared="true"] .c__avatar {
+ border-radius: 8px;
+ }
+
+ .mailbox-selector__text {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+ text-align: left;
+ }
+
+ .mailbox-selector__name {
+ font-size: var(--c--globals--font--sizes--sm);
+ line-height: 1.2;
+ font-weight: 700;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .mailbox-selector__email {
+ font-size: var(--c--globals--font--sizes--xs);
+ opacity: 0.8;
+ font-weight: 500;
+ line-height: 1.2;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+}
+
+// Same rounded-square treatment for shared mailboxes in the dropdown options.
+// The popover renders in a portal outside `.mailbox-selector`, so this rule is
+// intentionally global rather than nested under it.
+.mailbox-selector__option-avatar {
+ // Layout-transparent wrapper: the avatar sits exactly where the dropdown
+ // expects its icon, we only need the element as a styling hook.
+ display: contents;
+
+ &[data-shared="true"] .c__avatar {
+ border-radius: 8px;
+ }
+}
+
+// The dropdown caps its width at 320px, so a long mailbox name would otherwise
+// wrap onto several lines. Truncate it with an ellipsis instead. Scoped to our
+// items (which carry the avatar hook) since the popover lives in a portal.
+.c__dropdown-menu-item:has(.mailbox-selector__option-avatar) {
+ .c__dropdown-menu-item__label-container {
+ min-width: 0;
+ }
+
+ .c__dropdown-menu-item__label,
+ .c__dropdown-menu-item__label-subtext {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+}
diff --git a/src/frontend/src/features/layouts/components/mailbox-selector/index.tsx b/src/frontend/src/features/layouts/components/mailbox-selector/index.tsx
new file mode 100644
index 00000000..0e729dba
--- /dev/null
+++ b/src/frontend/src/features/layouts/components/mailbox-selector/index.tsx
@@ -0,0 +1,113 @@
+import { DropdownMenu, UserAvatar } from "@gouvfr-lasuite/ui-kit";
+import { ChevronDown } from "@gouvfr-lasuite/ui-kit/icons";
+import { Button } from "@gouvfr-lasuite/cunningham-react";
+import { useState } from "react";
+import { Mailbox } from "@/features/api/gen";
+import MailboxHelper from "@/features/utils/mailbox-helper";
+
+/** Display name of a mailbox, falling back to its address when no contact name
+ * is set (a mailbox may legitimately have a null/blank name). */
+const getMailboxLabel = (mailbox: Mailbox) => mailbox.name?.trim() || mailbox.email;
+
+type MailboxSelectorProps = {
+ /** Mailboxes the user can switch to (already the eligible subset). */
+ mailboxes: readonly Mailbox[];
+ /** Currently displayed mailbox. */
+ selectedMailbox: Mailbox;
+ /** Called with the picked mailbox id; never fired for the current one. */
+ onSelect: (mailboxId: string) => void | Promise
;
+};
+
+/**
+ * Mailbox switcher shared by the sidebar header and the settings modal: an
+ * avatar + bold name + address card that unfolds a dropdown of the eligible
+ * mailboxes. When the user owns a single mailbox there is nothing to switch to,
+ * so the card renders as static content (not a disabled button) to keep the
+ * name/address legible and avoid exposing a bogus "button, unavailable" control.
+ */
+export const MailboxSelector = ({
+ mailboxes,
+ selectedMailbox,
+ onSelect,
+}: MailboxSelectorProps) => {
+ const [isOpen, setIsOpen] = useState(false);
+
+ const label = getMailboxLabel(selectedMailbox);
+ const sublabel = selectedMailbox.name?.trim() ? selectedMailbox.email : null;
+ const canSwitch = mailboxes.length > 1;
+
+ // Avatar is decorative: the name it encodes is already shown next to it as
+ // text, so hide it from assistive tech to avoid a duplicate announcement.
+ const content = (
+ <>
+
+
+
+
+ {label}
+ {sublabel && {sublabel} }
+
+ >
+ );
+
+ if (!canSwitch) {
+ return (
+
+ );
+ }
+
+ const sortedMailboxes = MailboxHelper.sortByKind(mailboxes);
+ const options = sortedMailboxes.map((mailbox, index) => ({
+ label: getMailboxLabel(mailbox),
+ subText: mailbox.name?.trim() ? mailbox.email : undefined,
+ value: mailbox.id,
+ icon: (
+
+
+
+ ),
+ showSeparator: MailboxHelper.showSeparatorAfter(sortedMailboxes, index),
+ }));
+
+ return (
+
+ {
+ setIsOpen(false);
+ if (value !== selectedMailbox.id) {
+ void onSelect(value);
+ }
+ }}
+ >
+ }
+ iconPosition="right"
+ aria-haspopup="menu"
+ aria-expanded={isOpen}
+ fullWidth
+ onClick={() => setIsOpen(!isOpen)}
+ >
+ {content}
+
+
+
+ );
+};
diff --git a/src/frontend/src/features/layouts/components/mailbox-settings/modal-mailbox-settings/_index.scss b/src/frontend/src/features/layouts/components/mailbox-settings/modal-mailbox-settings/_index.scss
index 27f7bd82..2d9e6a7c 100644
--- a/src/frontend/src/features/layouts/components/mailbox-settings/modal-mailbox-settings/_index.scss
+++ b/src/frontend/src/features/layouts/components/mailbox-settings/modal-mailbox-settings/_index.scss
@@ -36,78 +36,14 @@
// macOS-style account card sitting at the very top of the settings sidebar:
// mailbox avatar on the left, name in bold and (when set) the address in a
// muted line underneath.
+ // Wrapper around the shared MailboxSelector in the sidebar header. Full width
+ // with min-width:0 so the selector's text column can shrink and ellipsize the
+ // long address instead of overflowing the narrow sidebar.
&__identity {
- /* gap: var(--c--globals--spacings--xs); */
width: 100%;
- // Min-width:0 lets the flexible text column actually shrink so the long
- // address can ellipsize instead of overflowing the narrow sidebar.
min-width: 0;
}
- &__identity-text {
- display: flex;
- flex-direction: column;
- align-items: flex-start;
- min-width: 0;
- line-height: 1.25;
- }
-
- &__identity-name {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- max-width: 100%;
- font-size: var(--c--globals--font--sizes--sm);
- font-weight: 600;
- color: var(--c--contextuals--content--semantic--neutral--primary);
- }
-
- &__identity-email {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- font-size: var(--c--globals--font--sizes--xs);
- font-weight: 400;
- min-width: 0;
- max-width: 100%;
- color: var(--c--contextuals--content--semantic--neutral--secondary);
- }
-
- // Shared box for both card variants so the switchable (button) and the
- // static (single-mailbox) renderings keep the exact same footprint.
- &__dropdown-button,
- &__identity-static {
- padding: var(--c--globals--spacings--sm) var(--c--globals--spacings--xs);
- }
-
- // Switchable variant: the whole card becomes a borderless button with an
- // unfold chevron and a subtle hover fill, mirroring a macOS popup button.
- &__dropdown-button {
- border: none;
- height: inherit;
- justify-content: space-between;
- }
-
- &__dropdown-button__content {
- display: flex;
- align-items: center;
- gap: var(--c--globals--spacings--s);
- // Let the content shrink inside the (full-width) card so the text column
- // can ellipsize instead of pushing the card wider than the sidebar.
- min-width: 0;
-
- // The avatar keeps its size; only the text column absorbs the shrink.
- & > :first-child {
- flex-shrink: 0;
- }
- }
-
- &__identity-chevron {
- margin-left: auto;
- flex-shrink: 0;
- color: var(--c--contextuals--content--semantic--neutral--tertiary);
- }
-
&__section {
margin-bottom: var(--c--globals--spacings--lg);
}
diff --git a/src/frontend/src/features/layouts/components/mailbox-settings/modal-mailbox-settings/index.tsx b/src/frontend/src/features/layouts/components/mailbox-settings/modal-mailbox-settings/index.tsx
index 67316958..e9ab8295 100644
--- a/src/frontend/src/features/layouts/components/mailbox-settings/modal-mailbox-settings/index.tsx
+++ b/src/frontend/src/features/layouts/components/mailbox-settings/modal-mailbox-settings/index.tsx
@@ -1,16 +1,14 @@
import {
- Button,
Modal,
ModalSize,
ModalTab,
} from "@gouvfr-lasuite/cunningham-react";
-import { DropdownMenu, HorizontalSeparator, UserAvatar } from "@gouvfr-lasuite/ui-kit";
+import { HorizontalSeparator } from "@gouvfr-lasuite/ui-kit";
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
-import { Mailbox } from "@/features/api/gen";
import { useMailboxContext } from "@/features/providers/mailbox";
import { FEATURE_KEYS, useFeatureFlag } from "@/hooks/use-feature";
-import MailboxHelper from "@/features/utils/mailbox-helper";
+import { MailboxSelector } from "@/features/layouts/components/mailbox-selector";
import {
useConfirmBeforeClose,
useConfirmUnsavedChanges,
@@ -21,7 +19,6 @@ import { MailboxSettingsSignaturesTab } from "./signatures-tab";
import { MailboxSettingsMessageTemplatesTab } from "./message-templates-tab";
import { MailboxSettingsAutorepliesTab } from "./autoreplies-tab";
import { MailboxSettingsIntegrationsTab } from "./integrations-tab";
-import { ChevronDown, ChevronUp } from "@gouvfr-lasuite/ui-kit/icons";
export type SettingsTabId =
| "general"
@@ -45,11 +42,6 @@ export const MODAL_MAILBOX_SETTINGS_ID = "modal-mailbox-settings";
// the sidebar↔content view itself can't be driven from outside the component.
const COMPACT_MODAL_MEDIA_QUERY = "(max-width: 576px)";
-/** Display name shown on the identity card; falls back to the address when the
- * mailbox has no contact name set. */
-const getMailboxName = (mailbox: Mailbox) =>
- mailbox.name?.trim() || mailbox.email;
-
/**
* Settings modal for a mailbox the user can configure. Built on Cunningham's
* "tab" modal layout: the sidebar lists setting categories (General — rename —,
@@ -91,7 +83,6 @@ export const ModalMailboxSettings = ({
const [selectedMailboxId, setSelectedMailboxId] = useState(
null,
);
- const [isMailboxDropdownOpen, setIsMailboxDropdownOpen] = useState(false);
const [isActiveTabDirty, setIsActiveTabDirty] = useState(false);
const confirmUnsavedChanges = useConfirmUnsavedChanges();
@@ -130,18 +121,6 @@ export const ModalMailboxSettings = ({
: "general";
});
- const mailboxOptions = useMemo(() => {
- const sortedMailboxes = MailboxHelper.sortByKind(settingsMailboxes);
-
- return sortedMailboxes.map((mailbox: Mailbox, index) => ({
- label: getMailboxName(mailbox),
- subText: mailbox.name?.trim() ? mailbox.email : undefined,
- value: mailbox.id,
- icon: ,
- showSeparator: MailboxHelper.showSeparatorAfter(sortedMailboxes, index),
- }));
- }, [settingsMailboxes]);
-
// Ordered ids of the tabs the selected mailbox exposes, gated by its abilities
// (and the integrations feature flag). Single source of truth: it drives both
// the rendered `tabs` below and the active-tab synchronisation on mailbox
@@ -191,81 +170,20 @@ export const ModalMailboxSettings = ({
return null;
}
- const hasMultipleMailboxes = settingsMailboxes.length > 1;
- const mailboxName = getMailboxName(settingsMailbox);
- const mailboxSubtitle = settingsMailbox.name?.trim()
- ? settingsMailbox.email
- : null;
-
- // The macOS-style account card: avatar, name and (when distinct) the address
- // underneath. Shared between the static and the switchable variants below.
- const identityCardContent = (
- <>
-
-
- {mailboxName}
- {mailboxSubtitle && (
-
- {mailboxSubtitle}
-
- )}
-
- >
- );
-
+ // macOS-style account card switching the configured mailbox, shared with the
+ // sidebar header switcher. Switching remounts the General tab and discards any
+ // unsaved rename, so confirm first when there are pending edits.
const sidebarHeader = (
<>
- {hasMultipleMailboxes ? (
- {
- setIsMailboxDropdownOpen(false);
- if (value === settingsMailbox.id) {
- return;
- }
- // Switching the configured mailbox remounts the General tab and
- // discards any unsaved rename, just like a tab switch does.
- if (await confirmUnsavedChanges(isActiveTabDirty)) {
- setSelectedMailboxId(value);
- }
- }}
- >
- setIsMailboxDropdownOpen(!isMailboxDropdownOpen)}
- icon={
- isMailboxDropdownOpen ? (
-
- ) : (
-
- )
- }
- iconPosition="right"
- >
-
- {identityCardContent}
-
-
-
- ) : (
- // Single mailbox: nothing to switch to, so the card is purely informative.
- // Rendered as static content (not a disabled button) to avoid exposing a
- // bogus "button, unavailable" control and to keep the name/address fully
- // legible instead of greyed out by the disabled state.
-
-
- {identityCardContent}
-
-
- )}
+ {
+ if (await confirmUnsavedChanges(isActiveTabDirty)) {
+ setSelectedMailboxId(value);
+ }
+ }}
+ />
>
);
diff --git a/src/frontend/src/styles/globals.scss b/src/frontend/src/styles/globals.scss
index a1b27efb..0fb3d3c2 100644
--- a/src/frontend/src/styles/globals.scss
+++ b/src/frontend/src/styles/globals.scss
@@ -16,6 +16,7 @@ body {
margin: 0;
background-color: var(--c--contextuals--background--surface--tertiary);
color: var(--c--contextuals--content--semantic--neutral--primary);
+ perspective: 9000px;
}
* {
diff --git a/src/frontend/src/styles/main.scss b/src/frontend/src/styles/main.scss
index 7259c38e..b8268a46 100644
--- a/src/frontend/src/styles/main.scss
+++ b/src/frontend/src/styles/main.scss
@@ -30,6 +30,7 @@
@use "./../features/ui/components/suggestion-input";
@use "./../features/ui/components/transient-tooltip";
@use "./../features/ui/components/assignees-avatar-group";
+@use "./../features/layouts/components/mailbox-selector";
@use "./../features/layouts/components/mailbox-panel";
@use "./../features/layouts/components/mailbox-panel/components/mailbox-actions";
@use "./../features/layouts/components/mailbox-panel/components/mailbox-list";