From 60b92e6ef4eeafab4cf13ead4cc93fa12ba0bd76 Mon Sep 17 00:00:00 2001
From: Linus Rath <139418639+rathlinus@users.noreply.github.com>
Date: Fri, 2 Oct 2026 13:01:57 +0200
Subject: [PATCH] fix: give widget buttons a 48dp tap area #52
Most widget buttons were 32dp tall (Archive, Delete, Reply and Next on
the triage card, Accept, Maybe and Decline, Send wishes, Mail my free
times), Up next's were 36dp and the round compose/new buttons in the
headers 28-32dp, all under the 48dp a finger needs, so taps landed
beside them and did nothing.
Button and Fab take a transparent tap area around what they draw
(`slop`), and the layouts give up the same amount of padding or gap
next to them, so the widgets look the same but the buttons take a 48dp
tap.
---
src/widgets/layouts/calendar.tsx | 10 ++++----
src/widgets/layouts/hubs.tsx | 2 +-
src/widgets/layouts/mail.tsx | 17 ++++++-------
src/widgets/layouts/plans.tsx | 20 +++++++++++-----
src/widgets/primitives.tsx | 41 ++++++++++++++++++++++++++++----
5 files changed, 66 insertions(+), 24 deletions(-)
diff --git a/src/widgets/layouts/calendar.tsx b/src/widgets/layouts/calendar.tsx
index 171a4437..ef25e63e 100644
--- a/src/widgets/layouts/calendar.tsx
+++ b/src/widgets/layouts/calendar.tsx
@@ -192,7 +192,7 @@ export const AgendaLayout: Layout = ({ s, p, f, now, height }) => {
iconName="calendar"
title={f.t('widgets.calendar.agenda', 'Agenda')}
click={open(links.calendar())}
- trailing={}
+ trailing={}
/>
);
const days = agendaDays(s.calendar.events, now, 14);
@@ -1008,7 +1008,8 @@ export const UpNextLayout: Layout = ({ s, p, f, now }) => {
: f.t('widgets.calendar.join', 'Join');
return (
-
+ // 36dp buttons in a 48dp tap area (slop 6), which takes 6dp of the bottom padding.
+
@@ -1040,9 +1041,9 @@ export const UpNextLayout: Layout = ({ s, p, f, now }) => {
{e.videoUrl ? (
-
+
) : (
-
+
)}
diff --git a/src/widgets/layouts/hubs.tsx b/src/widgets/layouts/hubs.tsx
index 1550e872..99deeb3b 100644
--- a/src/widgets/layouts/hubs.tsx
+++ b/src/widgets/layouts/hubs.tsx
@@ -432,7 +432,7 @@ export const RecentFilesLayout: Layout = ({ s, p, f, now, height }) => {
title={f.t('widgets.shortcuts.files', 'Files')}
count={f.t('widgets.files.recent', 'Recently changed')}
click={open(links.files())}
- trailing={}
+ trailing={}
/>
);
const items = s.files.items;
diff --git a/src/widgets/layouts/mail.tsx b/src/widgets/layouts/mail.tsx
index 50dcfeeb..23e28832 100644
--- a/src/widgets/layouts/mail.tsx
+++ b/src/widgets/layouts/mail.tsx
@@ -60,9 +60,9 @@ export const InboxLayout: Layout = ({ s, p, f, now, width, height }) => {
- {compact || narrow ? null : }
+ {compact || narrow ? null : }
-
+
)}
/>
@@ -115,7 +115,9 @@ export const TriageLayout: Layout = ({ s, p, f, now, width, widgetId, local }) =
const allFit = buttonWidth(labels.archive) + buttonWidth(labels.trash) + buttonWidth(labels.reply) <= room;
const replyFits = 2 * 32 + buttonWidth(labels.reply) <= room;
return (
-
+ // The buttons draw 32dp tall in a 48dp tap area (slop 8), which takes the
+ // gap above them and 8dp of the bottom padding.
+
@@ -139,15 +141,14 @@ export const TriageLayout: Layout = ({ s, p, f, now, width, widgetId, local }) =
{current.preview && !notice ? : null}
{notice ? : null}
-
-
+
-
+
-
+
-
+
);
diff --git a/src/widgets/layouts/plans.tsx b/src/widgets/layouts/plans.tsx
index b56a0214..4551989e 100644
--- a/src/widgets/layouts/plans.tsx
+++ b/src/widgets/layouts/plans.tsx
@@ -98,6 +98,8 @@ export const InvitationsLayout: Layout = ({ s, p, f, height }) => {
const pad = height < 140 ? 10 : 14;
const showPeople = inv.participants.length > 0 && height >= 132;
const buttonHeight = height < 116 ? 28 : 32;
+ // Tap area of 48dp around the buttons; it takes the bottom padding.
+ const slop = Math.min(pad, Math.round((48 - buttonHeight) / 2));
const label = (
inv.organizerName
@@ -111,7 +113,7 @@ export const InvitationsLayout: Layout = ({ s, p, f, height }) => {
const notice = noticeFor(s.notice, 'rsvp');
return (
-
+
{
label={f.t('widgets.invites.accept', 'Accept')}
flex={1}
height={buttonHeight}
+ slop={slop}
click={reply('accepted')}
/>
-
+
-
+
);
@@ -360,7 +363,9 @@ export const FreeTimeLayout: Layout = ({ s, p, f, now, width, height }) => {
};
return (
-
+ // The full-width button draws tier.button tall; its tap area also takes
+ // the bottom padding (slop).
+
{
) : null}
- {tier.button ? : null}
{tier.button ? (
@@ -437,6 +441,8 @@ export const FreeTimeLayout: Layout = ({ s, p, f, now, width, height }) => {
label={f.t('widgets.free.mail', 'Mail my free times')}
flex={1}
height={tier.button}
+ // Above, the tap area takes the gap and any height to spare; below, the padding.
+ slop={{ top: Math.min(tier.pad, tier.gap + Math.max(0, height - freeTierHeight(tier))), bottom: tier.pad }}
click={mail}
/>
@@ -481,7 +487,7 @@ export const TasksLayout: Layout = ({ s, p, f, now, height }) => {
title={f.t('widgets.tasks.title', 'Tasks')}
count={f.t('widgets.tasks.open_count', '{count} open', { count: openCount })}
click={open(links.calendar())}
- trailing={}
+ trailing={}
/>
{notice ? (
@@ -665,6 +671,8 @@ export const BirthdaysLayout: Layout = ({ s, p, f, now, width, height }) => {
variant="primary"
iconName="send"
label={narrow ? undefined : f.t('widgets.birthdays.send_wishes', 'Send wishes')}
+ // The row is 50dp; the tap area takes its full height.
+ slop={Math.floor((BIRTHDAY_ROW_HEIGHT - 32) / 2)}
click={open(links.compose({ to: wish, subject: f.t('widgets.birthdays.wishes_subject', 'Happy birthday!') }))}
/>
) : null}
diff --git a/src/widgets/primitives.tsx b/src/widgets/primitives.tsx
index dd5b401b..e82b8dfc 100644
--- a/src/widgets/primitives.tsx
+++ b/src/widgets/primitives.tsx
@@ -222,6 +222,7 @@ export function Button({
click,
height = 32,
flex,
+ slop = 0,
}: {
p: WidgetPalette;
label?: string;
@@ -230,18 +231,26 @@ export function Button({
click?: WidgetClick;
height?: number;
flex?: number;
+ /**
+ * Transparent tap area above and below the drawn button, so a 32dp button
+ * still takes a 48dp tap. Take the same amount off the spacing around it.
+ */
+ slop?: number | { top: number; bottom: number };
}) {
const primary = variant === 'primary';
+ const [slopTop, slopBottom] = typeof slop === 'number' ? [slop, slop] : [slop.top, slop.bottom];
const fg = primary ? p.primaryFg : p.fg;
- return (
+ const face = (
: null}
);
+ if (!slop) return face;
+ return (
+
+ {face}
+
+ );
}
/** Round primary action (compose, new event), the webmail's floating button. */
@@ -260,15 +282,18 @@ export function Fab({
iconName,
size = 32,
click,
+ slop = 0,
}: {
p: WidgetPalette;
iconName: IconName;
size?: number;
click?: WidgetClick;
+ /** Transparent tap area around the button (see Button). */
+ slop?: number;
}) {
- return (
+ const face = (
);
+ if (!slop) return face;
+ return (
+
+ {face}
+
+ );
}
/** Icon-only ghost button (search in a header). */