Let the contact list be resized, and give the contact the rest of the page

The contacts grid still had three columns from when the address books sat
inside the view: 220px for them, 280-360px for the list, the rest for the
contact. The books moved to the app's left pane in 350f4f4 and the grid
never followed, so the list was squeezed into the books' 220px, truncating
names, the contact was held to 360px, and the remaining width sat empty.

It is two columns now, with the same splitter as the message list between
them: drag between 240px and the width that leaves 360px for the contact,
arrow keys move it, double-click puts it back to 320px. The width is a
device setting beside the mail list's, so it is kept by a device marked as
your own and never synced. The splitter is hidden where contacts show one
pane at a time. The unused .contacts-books rules are gone.

"Resize contact list" is in all nine catalogues.
This commit is contained in:
2026-09-15 07:24:56 -07:00
parent 14e22c21fe
commit 95395200a3
13 changed files with 43 additions and 6 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ describe("which settings follow the account", () => {
const synced = syncedPart(DEFAULT_SETTINGS); const synced = syncedPart(DEFAULT_SETTINGS);
// A pane width picked on a monitor is wrong on a laptop, and the // A pane width picked on a monitor is wrong on a laptop, and the
// notification toggles track a per-browser permission grant. // notification toggles track a per-browser permission grant.
for (const key of ["listPaneWidth", "listPaneHeight", "sidebarWidth", "density", "fontSize", "sidebarCollapsed", "desktopNotifications", "notificationSound"]) { for (const key of ["listPaneWidth", "listPaneHeight", "contactsListWidth", "sidebarWidth", "density", "fontSize", "sidebarCollapsed", "desktopNotifications", "notificationSound"]) {
expect(synced, key).not.toHaveProperty(key); expect(synced, key).not.toHaveProperty(key);
} }
}); });
+1
View File
@@ -1215,6 +1215,7 @@ export const catalog: Catalog = {
"Full screen": "Vollbild", "Full screen": "Vollbild",
"Resize panes": "Bereichsgröße ändern", "Resize panes": "Bereichsgröße ändern",
"Resize message list": "Größe der Nachrichtenliste ändern", "Resize message list": "Größe der Nachrichtenliste ändern",
"Resize contact list": "Größe der Kontaktliste ändern",
"Resize sidebar": "Größe der Seitenleiste ändern", "Resize sidebar": "Größe der Seitenleiste ändern",
"Hide from list": "Aus der Liste ausblenden", "Hide from list": "Aus der Liste ausblenden",
"Show in list": "In der Liste einblenden", "Show in list": "In der Liste einblenden",
+1
View File
@@ -1188,6 +1188,7 @@ export const catalog: Catalog = {
"Full screen": "Pantalla completa", "Full screen": "Pantalla completa",
"Resize panes": "Cambiar el tamaño de los paneles", "Resize panes": "Cambiar el tamaño de los paneles",
"Resize message list": "Cambiar el tamaño de la lista de mensajes", "Resize message list": "Cambiar el tamaño de la lista de mensajes",
"Resize contact list": "Cambiar el tamaño de la lista de contactos",
"Resize sidebar": "Cambiar el tamaño de la barra lateral", "Resize sidebar": "Cambiar el tamaño de la barra lateral",
"Hide from list": "Ocultar de la lista", "Hide from list": "Ocultar de la lista",
"Show in list": "Mostrar en la lista", "Show in list": "Mostrar en la lista",
+1
View File
@@ -1193,6 +1193,7 @@ export const catalog: Catalog = {
"Full screen": "Plein écran", "Full screen": "Plein écran",
"Resize panes": "Redimensionner les volets", "Resize panes": "Redimensionner les volets",
"Resize message list": "Redimensionner la liste des messages", "Resize message list": "Redimensionner la liste des messages",
"Resize contact list": "Redimensionner la liste des contacts",
"Resize sidebar": "Redimensionner la barre latérale", "Resize sidebar": "Redimensionner la barre latérale",
"Hide from list": "Masquer de la liste", "Hide from list": "Masquer de la liste",
"Show in list": "Afficher dans la liste", "Show in list": "Afficher dans la liste",
+1
View File
@@ -1196,6 +1196,7 @@ export const catalog: Catalog = {
"Full screen": "全画面", "Full screen": "全画面",
"Resize panes": "ペインの大きさを変更", "Resize panes": "ペインの大きさを変更",
"Resize message list": "メール一覧の大きさを変更", "Resize message list": "メール一覧の大きさを変更",
"Resize contact list": "連絡先一覧の大きさを変更",
"Resize sidebar": "サイドバーの大きさを変更", "Resize sidebar": "サイドバーの大きさを変更",
"Hide from list": "一覧で非表示にする", "Hide from list": "一覧で非表示にする",
"Show in list": "一覧に表示する", "Show in list": "一覧に表示する",
+1
View File
@@ -1184,6 +1184,7 @@ export const catalog: Catalog = {
"Full screen": "Volledig scherm", "Full screen": "Volledig scherm",
"Resize panes": "Vensterdelen verslepen", "Resize panes": "Vensterdelen verslepen",
"Resize message list": "Grootte van de berichtenlijst wijzigen", "Resize message list": "Grootte van de berichtenlijst wijzigen",
"Resize contact list": "Grootte van de contactenlijst wijzigen",
"Resize sidebar": "Grootte van de zijbalk wijzigen", "Resize sidebar": "Grootte van de zijbalk wijzigen",
"Hide from list": "Verbergen in de lijst", "Hide from list": "Verbergen in de lijst",
"Show in list": "Tonen in de lijst", "Show in list": "Tonen in de lijst",
+1
View File
@@ -1191,6 +1191,7 @@ export const catalog: Catalog = {
"Full screen": "Tela cheia", "Full screen": "Tela cheia",
"Resize panes": "Redimensionar os painéis", "Resize panes": "Redimensionar os painéis",
"Resize message list": "Redimensionar a lista de mensagens", "Resize message list": "Redimensionar a lista de mensagens",
"Resize contact list": "Redimensionar a lista de contatos",
"Resize sidebar": "Redimensionar a barra lateral", "Resize sidebar": "Redimensionar a barra lateral",
"Hide from list": "Ocultar da lista", "Hide from list": "Ocultar da lista",
"Show in list": "Mostrar na lista", "Show in list": "Mostrar na lista",
+1
View File
@@ -1190,6 +1190,7 @@ export const catalog: Catalog = {
"Full screen": "Во весь экран", "Full screen": "Во весь экран",
"Resize panes": "Изменить размер панелей", "Resize panes": "Изменить размер панелей",
"Resize message list": "Изменить размер списка писем", "Resize message list": "Изменить размер списка писем",
"Resize contact list": "Изменить размер списка контактов",
"Resize sidebar": "Изменить размер боковой панели", "Resize sidebar": "Изменить размер боковой панели",
"Hide from list": "Скрыть из списка", "Hide from list": "Скрыть из списка",
"Show in list": "Показать в списке", "Show in list": "Показать в списке",
+1
View File
@@ -1184,6 +1184,7 @@ export const catalog: Catalog = {
"Full screen": "На весь екран", "Full screen": "На весь екран",
"Resize panes": "Змінити розмір панелей", "Resize panes": "Змінити розмір панелей",
"Resize message list": "Змінити розмір списку листів", "Resize message list": "Змінити розмір списку листів",
"Resize contact list": "Змінити розмір списку контактів",
"Resize sidebar": "Змінити розмір бічної панелі", "Resize sidebar": "Змінити розмір бічної панелі",
"Hide from list": "Сховати зі списку", "Hide from list": "Сховати зі списку",
"Show in list": "Показати в списку", "Show in list": "Показати в списку",
+1
View File
@@ -1195,6 +1195,7 @@ export const catalog: Catalog = {
"Full screen": "全屏", "Full screen": "全屏",
"Resize panes": "调整窗格大小", "Resize panes": "调整窗格大小",
"Resize message list": "调整邮件列表大小", "Resize message list": "调整邮件列表大小",
"Resize contact list": "调整联系人列表大小",
"Resize sidebar": "调整侧边栏大小", "Resize sidebar": "调整侧边栏大小",
"Hide from list": "从列表中隐藏", "Hide from list": "从列表中隐藏",
"Show in list": "在列表中显示", "Show in list": "在列表中显示",
+4
View File
@@ -256,6 +256,8 @@ export interface Settings {
listPaneWidth: number; listPaneWidth: number;
/** Height (px) of the message list when the reading pane is below. */ /** Height (px) of the message list when the reading pane is below. */
listPaneHeight: number; listPaneHeight: number;
/** Width (px) of the contact list, beside the contact on show. */
contactsListWidth: number;
/** /**
* Width (px) of the sidebar, dragged by its edge (#345). Null until someone * Width (px) of the sidebar, dragged by its edge (#345). Null until someone
* drags it, and null again after a double-click resets it -- which leaves the * drags it, and null again after a double-click resets it -- which leaves the
@@ -377,6 +379,7 @@ export const DEFAULT_SETTINGS: Settings = {
sendAndArchive: false, sendAndArchive: false,
listPaneWidth: 520, listPaneWidth: 520,
listPaneHeight: 340, listPaneHeight: 340,
contactsListWidth: 320,
sidebarWidth: null, sidebarWidth: null,
eventCategories: [ eventCategories: [
{ name: "Important", color: "#dc2626" }, { name: "Important", color: "#dc2626" },
@@ -408,6 +411,7 @@ export const DEVICE_KEYS: ReadonlySet<keyof Settings> = new Set<keyof Settings>(
"notificationSound", "notificationSound",
"listPaneWidth", "listPaneWidth",
"listPaneHeight", "listPaneHeight",
"contactsListWidth",
"sidebarWidth", "sidebarWidth",
]); ]);
+2 -4
View File
@@ -1753,8 +1753,7 @@ select optgroup { background-color: var(--bg-elev); color: var(--fg); }
/* ========================================================================== /* ==========================================================================
Contacts Contacts
========================================================================== */ ========================================================================== */
.contacts-layout { display: grid; grid-template-columns: 220px minmax(280px, 360px) 1fr; height: 100%; min-height: 0; flex: 1; } .contacts-layout { display: grid; grid-template-columns: var(--list-size, 320px) auto minmax(0, 1fr); height: 100%; min-height: 0; flex: 1; }
.contacts-books { border-right: 1px solid var(--border); padding: 12px 8px; overflow-y: auto; overflow-x: hidden; }
.contacts-list { border-right: 1px solid var(--border); display: flex; flex-direction: column; min-height: 0; } .contacts-list { border-right: 1px solid var(--border); display: flex; flex-direction: column; min-height: 0; }
.contacts-list .list-search { padding: 10px 12px; border-bottom: 1px solid var(--border); } .contacts-list .list-search { padding: 10px 12px; border-bottom: 1px solid var(--border); }
.contacts-scroll { flex: 1; overflow-y: auto; } .contacts-scroll { flex: 1; overflow-y: auto; }
@@ -2015,7 +2014,6 @@ select optgroup { background-color: var(--bg-elev); color: var(--fg); }
@media (max-width: 1100px) { @media (max-width: 1100px) {
:root { --list-w: 44%; } :root { --list-w: 44%; }
.msg-row .msg-from { width: 140px; } .msg-row .msg-from { width: 140px; }
.contacts-layout { grid-template-columns: 200px 300px 1fr; }
} }
@media (max-width: 900px) { @media (max-width: 900px) {
.mail-layout:not(.pane-off):not(.pane-bottom) { grid-template-columns: 1fr !important; } .mail-layout:not(.pane-off):not(.pane-bottom) { grid-template-columns: 1fr !important; }
@@ -2036,7 +2034,7 @@ select optgroup { background-color: var(--bg-elev); color: var(--fg); }
.admin-table .hide-mobile { display: none; } .admin-table .hide-mobile { display: none; }
.admin-sheet { width: 100%; border-left: 0; box-shadow: none; } .admin-sheet { width: 100%; border-left: 0; box-shadow: none; }
.contacts-layout { grid-template-columns: 1fr; } .contacts-layout { grid-template-columns: 1fr; }
.contacts-books { display: none; } .contacts-layout .splitter { display: none; }
.contacts-layout.detail .contacts-list { display: none; } .contacts-layout.detail .contacts-list { display: none; }
.contacts-layout:not(.detail) .contact-detail { display: none; } .contacts-layout:not(.detail) .contact-detail { display: none; }
.cal-layout { grid-template-columns: 1fr; } .cal-layout { grid-template-columns: 1fr; }
+27 -1
View File
@@ -10,6 +10,8 @@ import { formatDate, formatDateLong } from "@/lib/datetime";
import { Avatar, Empty, Spinner, useIsNarrow } from "@/ui/misc"; import { Avatar, Empty, Spinner, useIsNarrow } from "@/ui/misc";
import { confirmDialog } from "@/ui/dialog"; import { confirmDialog } from "@/ui/dialog";
import { toast } from "@/ui/toast"; import { toast } from "@/ui/toast";
import { Splitter } from "@/ui/Splitter";
import { useSettings } from "@/store/settings";
import { ContactEditor } from "./ContactEditor"; import { ContactEditor } from "./ContactEditor";
import { avatarColor } from "@/lib/address"; import { avatarColor } from "@/lib/address";
import { plural, t as translate } from "@/lib/i18n"; import { plural, t as translate } from "@/lib/i18n";
@@ -18,6 +20,14 @@ export function ContactsView({ id }: { id?: string }) {
const [, navigate] = useLocation(); const [, navigate] = useLocation();
const contacts = useContacts(); const contacts = useContacts();
const narrow = useIsNarrow(); const narrow = useIsNarrow();
const listWidth = useSettings((s) => s.settings.contactsListWidth);
const updateSettings = useSettings((s) => s.update);
const layoutRef = useRef<HTMLDivElement>(null);
/* The width mid-drag, and a ref mirroring it for the end of a key press,
which follows the resize in the same tick -- the same pair as the mail
list's splitter, for the same reason. */
const [liveWidth, setLiveWidth] = useState<number | null>(null);
const liveWidthRef = useRef<number | null>(null);
const [q, setQ] = useState(""); const [q, setQ] = useState("");
/* The book being shown lives in the store, because the list that chooses it /* The book being shown lives in the store, because the list that chooses it
is the app's own sidebar rather than anything this view owns. */ is the app's own sidebar rather than anything this view owns. */
@@ -240,8 +250,23 @@ export function ContactsView({ id }: { id?: string }) {
} }
}; };
const shownListWidth = liveWidth ?? listWidth;
const onSplit = (delta: number) => {
const total = layoutRef.current?.clientWidth ?? 1200;
const max = Math.max(240, total - 360);
const next = Math.min(max, Math.max(240, (liveWidthRef.current ?? shownListWidth) + delta));
liveWidthRef.current = next;
setLiveWidth(next);
};
const onSplitEnd = () => {
const width = liveWidthRef.current;
liveWidthRef.current = null;
setLiveWidth(null);
if (width != null) updateSettings({ contactsListWidth: width });
};
return ( return (
<div className={`contacts-layout ${selected || editing ? "detail" : ""}`}> <div ref={layoutRef} className={`contacts-layout ${selected || editing ? "detail" : ""}`} style={{ "--list-size": `${shownListWidth}px` } as React.CSSProperties}>
<section className="contacts-list"> <section className="contacts-list">
{pickedIds.length ? ( {pickedIds.length ? (
@@ -303,6 +328,7 @@ export function ContactsView({ id }: { id?: string }) {
))} ))}
</div> </div>
</section> </section>
{!narrow && <Splitter direction="vertical" onResize={onSplit} onEnd={onSplitEnd} onReset={() => updateSettings({ contactsListWidth: 320 })} ariaLabel={translate("Resize contact list")} />}
<section className="contact-detail"> <section className="contact-detail">
{selected ? ( {selected ? (