From 5d12e61644ecaabcbe7a71868ae42ece63a83d61 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste PENRATH Date: Fri, 12 Jun 2026 00:16:50 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=85(frontend)=20refine=20mailbox=20dro?= =?UTF-8?q?pdown=20menu=20(#705)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Customize mailbox dropdown menu to make it more 'interactible'. Distinguish shared vs personal mailbox through avatar shape and display Name + email. --- .../src/__tests__/mailbox-settings.spec.ts | 6 +- src/e2e/src/__tests__/message-import.spec.ts | 5 +- .../__tests__/message-inline-image.spec.ts | 2 +- src/e2e/src/__tests__/message-send.spec.ts | 2 +- src/e2e/src/__tests__/thread-event.spec.ts | 3 +- src/e2e/src/utils-test.ts | 14 ++- .../components/mailbox-panel/_index.scss | 32 +---- .../components/mailbox-panel/index.tsx | 54 ++------- .../components/mailbox-selector/_index.scss | 99 +++++++++++++++ .../components/mailbox-selector/index.tsx | 113 ++++++++++++++++++ .../modal-mailbox-settings/_index.scss | 70 +---------- .../modal-mailbox-settings/index.tsx | 110 +++-------------- src/frontend/src/styles/globals.scss | 1 + src/frontend/src/styles/main.scss | 1 + 14 files changed, 272 insertions(+), 240 deletions(-) create mode 100644 src/frontend/src/features/layouts/components/mailbox-selector/_index.scss create mode 100644 src/frontend/src/features/layouts/components/mailbox-selector/index.tsx 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) }); - }} - > - - -
+ { 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 ( +
+
+ {content} +
+
+ ); + } + + 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); + } + }} + > + + +
+ ); +}; 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); - } - }} - > - - - ) : ( - // 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";