From 49b5a902d658385a7285256ea433cd57622b3972 Mon Sep 17 00:00:00 2001
From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com>
Date: Mon, 7 Sep 2026 16:36:45 +0530
Subject: [PATCH 1/3] fix(drawer): scroll area owns the L/16 padding so button
shadows never clip
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Root cause: DrawerContent's overflow-auto scroll wrapper clips painting
at its own box edge. Consumers put the horizontal padding on the panel
AROUND that wrapper (or on the panel via className), so a w-full
button's 4px offset shadow fell outside the clip box and was cut in a
straight vertical line — the avatar drawer, KYC status drawer, token
selector and QR drawer all hit or worked around it locally.
Fix: the scroll wrapper now carries the design.md container padding
(px-4, L/16) INSIDE the overflow box, so shadows always land in the
padded, unclipped region. DrawerHeader/DrawerFooter drop their own
horizontal padding (py-4) to match. Every consumer's now-redundant
horizontal padding is removed — content width is unchanged everywhere
(16px moved inside the clip box, not added). Local workarounds retired:
AvatarPicker's scrollAreaClassName px-4, QRBottomDrawer's pr-1 gutter.
KycStatusDrawer's pb-12 moved into the scroll area so the retry
button's bottom shadow paints too.
---
.../(mobile-ui)/dev/ds/patterns/drawer/page.tsx | 4 ++--
.../components/OnrampConfirmationModal.tsx | 2 +-
src/components/Avatar/AvatarPicker.tsx | 10 +++++-----
src/components/Badges/BadgeStatusDrawer.tsx | 2 +-
src/components/Card/CardUnlockDrawer.tsx | 2 +-
.../Global/CancelSendLinkDrawer/index.tsx | 2 +-
src/components/Global/Drawer/index.tsx | 14 +++++++++++---
src/components/Global/QRBottomDrawer/index.tsx | 10 +++++-----
.../Global/TokenSelector/TokenSelector.tsx | 2 +-
.../IdentityVerification/UnlockMethodModal.tsx | 2 +-
src/components/Kyc/InitiateKycModal.tsx | 4 ++--
src/components/Kyc/KycStatusDrawer.tsx | 2 +-
.../Kyc/modals/KycActionRequiredModal.tsx | 2 +-
src/components/Kyc/modals/KycFailedModal.tsx | 2 +-
src/components/Kyc/modals/KycProcessingModal.tsx | 2 +-
.../Kyc/modals/KycRegionRestrictedModal.tsx | 2 +-
.../Profile/components/PublicProfile.tsx | 2 +-
.../TransactionDetailsDrawer.tsx | 2 +-
.../provider-actions/CancelDepositActions.tsx | 2 +-
src/dev/surfaces/options/OnrampOptions.tsx | 2 +-
src/features/home/components/HomeActionDrawers.tsx | 2 +-
.../components/ContributorsDrawer.tsx | 2 +-
22 files changed, 42 insertions(+), 34 deletions(-)
diff --git a/src/app/(mobile-ui)/dev/ds/patterns/drawer/page.tsx b/src/app/(mobile-ui)/dev/ds/patterns/drawer/page.tsx
index 36ab305db9..1f4d2ab550 100644
--- a/src/app/(mobile-ui)/dev/ds/patterns/drawer/page.tsx
+++ b/src/app/(mobile-ui)/dev/ds/patterns/drawer/page.tsx
@@ -41,7 +41,7 @@ export default function DrawerPage() {
This is a vaul-based bottom sheet. Swipe down to dismiss.
-
+
The Drawer component wraps vaul and provides a consistent bottom-sheet experience.
It includes an overlay, drag handle, and max-height constraint (80vh).
@@ -83,7 +83,7 @@ export default function DrawerPage() {
Title
Description
-
+
{/* Content */}
diff --git a/src/components/AddMoney/components/OnrampConfirmationModal.tsx b/src/components/AddMoney/components/OnrampConfirmationModal.tsx
index e4364e1286..725ee355bb 100644
--- a/src/components/AddMoney/components/OnrampConfirmationModal.tsx
+++ b/src/components/AddMoney/components/OnrampConfirmationModal.tsx
@@ -32,7 +32,7 @@ export const OnrampConfirmationModal = ({
}}
>
-
+
{/* the head carries M/12 beneath it; the slide keeps the L/16 of the outer stack */}
diff --git a/src/components/Avatar/AvatarPicker.tsx b/src/components/Avatar/AvatarPicker.tsx
index cfb12d2a7c..4c44602aa4 100644
--- a/src/components/Avatar/AvatarPicker.tsx
+++ b/src/components/Avatar/AvatarPicker.tsx
@@ -165,11 +165,11 @@ export function AvatarPicker({ open, onOpenChange }: AvatarPickerProps) {
return (
- {/* The horizontal padding belongs to the SCROLL AREA, not to the panel
- around it: the panel's padding sits outside the overflow-auto box,
- so a w-full button's 4px offset shadow fell past the scroll edge
- and got clipped. The matching pb-2 below covers the bottom. */}
-
+ {/* DrawerContent's scroll area owns the horizontal padding now
+ (shadow-clip fix) — only the vertical padding is ours. The
+ pb-2 on the content below keeps the last button's bottom
+ shadow inside the scroll box. */}
+
{t('title')}
{t('description')}
diff --git a/src/components/Badges/BadgeStatusDrawer.tsx b/src/components/Badges/BadgeStatusDrawer.tsx
index 30fb4a9f91..8a7f8da475 100644
--- a/src/components/Badges/BadgeStatusDrawer.tsx
+++ b/src/components/Badges/BadgeStatusDrawer.tsx
@@ -61,7 +61,7 @@ export const BadgeStatusDrawer = ({ isOpen, onClose, badge }: BadgeStatusDrawerP
py-4 + p-4 here stacked 32px of extra head room (PR #2813
review, Jota). */}
-
+
{/* centered head per the TX Details chrome (board 17490:115877):
badge art → one-line title. Tapping it opens the detail
modal — close the unlock drawer (z-50) first so the modal
diff --git a/src/components/Card/CardUnlockDrawer.tsx b/src/components/Card/CardUnlockDrawer.tsx
index b36d92efc5..ff3c503d02 100644
--- a/src/components/Card/CardUnlockDrawer.tsx
+++ b/src/components/Card/CardUnlockDrawer.tsx
@@ -72,7 +72,7 @@ export const CardUnlockDrawer: FC
= ({ isOpen, onClose, entry, username,
doesn't blow up to 432px tall at the drawer's full xl
width and force a scrollbar. Centred so the buttons
below it span the same content column. */}
-
+
-
+
{/* destructive-confirm anatomy: red icon bubble (design.md nested-drawer recipe) */}
diff --git a/src/components/Global/Drawer/index.tsx b/src/components/Global/Drawer/index.tsx
index ef558b4867..4c9eb26714 100644
--- a/src/components/Global/Drawer/index.tsx
+++ b/src/components/Global/Drawer/index.tsx
@@ -116,10 +116,17 @@ const DrawerContent = React.forwardRef
{accessibleTitle}}
+ {/* The scroll wrapper owns the horizontal L/16 container
+ * padding (design.md spacing table). It must live HERE,
+ * inside the overflow box: overflow-auto clips painting at
+ * its own edge, so padding on the panel around it leaves a
+ * w-full button's 4px offset shadow outside the clip box —
+ * cut off in a straight line. Consumers must not re-add
+ * horizontal padding on the panel or on their content. */}
@@ -138,7 +145,8 @@ DrawerContent.displayName = 'DrawerContent'
// own head instead — see CancelSendLinkDrawer and KycRegionRestrictedModal.
const DrawerHeader = ({ className, ...props }: React.HTMLAttributes
) => (
@@ -146,7 +154,7 @@ const DrawerHeader = ({ className, ...props }: React.HTMLAttributes) => (
-
+
)
DrawerFooter.displayName = 'DrawerFooter'
diff --git a/src/components/Global/QRBottomDrawer/index.tsx b/src/components/Global/QRBottomDrawer/index.tsx
index 1d6ce0c8d3..079811c1e8 100644
--- a/src/components/Global/QRBottomDrawer/index.tsx
+++ b/src/components/Global/QRBottomDrawer/index.tsx
@@ -105,7 +105,7 @@ const QRBottomDrawer = ({ url, title, text, buttonText, className }: QRBottomDra
reaches the cap through a CSS variable because Tailwind only emits an
arbitrary value it can read literally in the source. */}
{title}
- {/* the button's shadow is offset 4px right AND 4px down, so the
- drawer's overflow-auto scroll wrapper clips it on both edges
- without a gutter on each */}
-
+ {/* the scroll wrapper's default px-4 covers the button
+ shadow's right edge now; pb-1 still covers the 4px
+ bottom offset */}
+
{text}
diff --git a/src/components/Global/TokenSelector/TokenSelector.tsx b/src/components/Global/TokenSelector/TokenSelector.tsx
index f30759af1f..cbcf0b5573 100644
--- a/src/components/Global/TokenSelector/TokenSelector.tsx
+++ b/src/components/Global/TokenSelector/TokenSelector.tsx
@@ -477,7 +477,7 @@ const TokenSelector: React.FC
= ({ classNameButton, viewT
-
+
{showNetworkList ? (
-
+
{/* the head owns the M/12 beneath it; everything after it
keeps the drawer's L/16 rhythm */}
diff --git a/src/components/Kyc/InitiateKycModal.tsx b/src/components/Kyc/InitiateKycModal.tsx
index 2c70015332..fb85128d45 100644
--- a/src/components/Kyc/InitiateKycModal.tsx
+++ b/src/components/Kyc/InitiateKycModal.tsx
@@ -218,7 +218,7 @@ export const InitiateKycModal = ({
}}
>
-
+
{t('degraded.title')}
@@ -333,7 +333,7 @@ export const InitiateKycModal = ({
}}
>
-
+
{/* the head owns the M/12 beneath it; everything after keeps
the drawer's L/16 rhythm */}
diff --git a/src/components/Kyc/KycStatusDrawer.tsx b/src/components/Kyc/KycStatusDrawer.tsx
index 2e815c12ce..2e3cffa8d7 100644
--- a/src/components/Kyc/KycStatusDrawer.tsx
+++ b/src/components/Kyc/KycStatusDrawer.tsx
@@ -95,7 +95,7 @@ export const KycStatusDrawer = ({ isOpen, onClose, onKeepMounted }: KycStatusDra
return (
<>
-
+
{renderContent()}
{sumsubFlow.error && (
{sumsubFlow.error}
diff --git a/src/components/Kyc/modals/KycActionRequiredModal.tsx b/src/components/Kyc/modals/KycActionRequiredModal.tsx
index ecb11f522c..476a9902a4 100644
--- a/src/components/Kyc/modals/KycActionRequiredModal.tsx
+++ b/src/components/Kyc/modals/KycActionRequiredModal.tsx
@@ -31,7 +31,7 @@ export const KycActionRequiredModal = ({
}}
>
-
+
{/* the head owns the M/12 beneath it; everything after it
keeps the drawer's L/16 rhythm */}
diff --git a/src/components/Kyc/modals/KycFailedModal.tsx b/src/components/Kyc/modals/KycFailedModal.tsx
index 020785ff6b..5b5c3f68e6 100644
--- a/src/components/Kyc/modals/KycFailedModal.tsx
+++ b/src/components/Kyc/modals/KycFailedModal.tsx
@@ -44,7 +44,7 @@ export const KycFailedModal = ({
}}
>
-
+
{/* the head owns the M/12 beneath it; everything after it
keeps the drawer's L/16 rhythm */}
diff --git a/src/components/Kyc/modals/KycProcessingModal.tsx b/src/components/Kyc/modals/KycProcessingModal.tsx
index 833433cc13..ef5bcd4786 100644
--- a/src/components/Kyc/modals/KycProcessingModal.tsx
+++ b/src/components/Kyc/modals/KycProcessingModal.tsx
@@ -21,7 +21,7 @@ export const KycProcessingModal = ({ visible, onClose }: KycProcessingModalProps
}}
>
-
+
{/* the head owns the M/12 beneath it; everything after it
keeps the drawer's L/16 rhythm */}
diff --git a/src/components/Kyc/modals/KycRegionRestrictedModal.tsx b/src/components/Kyc/modals/KycRegionRestrictedModal.tsx
index 337dcc0cd0..7be9e88075 100644
--- a/src/components/Kyc/modals/KycRegionRestrictedModal.tsx
+++ b/src/components/Kyc/modals/KycRegionRestrictedModal.tsx
@@ -29,7 +29,7 @@ export const KycRegionRestrictedModal = ({ visible, onClose }: KycRegionRestrict
}}
>
-
+
{/* DrawerHeader carries the M/12; the cta keeps the L/16 of the outer stack */}
diff --git a/src/components/Profile/components/PublicProfile.tsx b/src/components/Profile/components/PublicProfile.tsx
index 4456b8c19a..72784abdd9 100644
--- a/src/components/Profile/components/PublicProfile.tsx
+++ b/src/components/Profile/components/PublicProfile.tsx
@@ -294,7 +294,7 @@ const PublicProfile: React.FC
= ({ username, isLoggedIn = fa
}}
>
-
+
{t('noInviteTitle')}
diff --git a/src/components/TransactionDetails/TransactionDetailsDrawer.tsx b/src/components/TransactionDetails/TransactionDetailsDrawer.tsx
index df14ac9337..c8371a196f 100644
--- a/src/components/TransactionDetails/TransactionDetailsDrawer.tsx
+++ b/src/components/TransactionDetails/TransactionDetailsDrawer.tsx
@@ -67,7 +67,7 @@ export const TransactionDetailsDrawer: React.FC =
isModalOpen={isModalOpen}
setIsModalOpen={setIsModalOpen}
avatarUrl={avatarUrl}
- className="px-4 pb-4"
+ className="pb-4"
/>
diff --git a/src/components/TransactionDetails/provider-actions/CancelDepositActions.tsx b/src/components/TransactionDetails/provider-actions/CancelDepositActions.tsx
index 905671eae4..012210b38e 100644
--- a/src/components/TransactionDetails/provider-actions/CancelDepositActions.tsx
+++ b/src/components/TransactionDetails/provider-actions/CancelDepositActions.tsx
@@ -137,7 +137,7 @@ export function CancelDepositActions({
}}
>
-
+
diff --git a/src/dev/surfaces/options/OnrampOptions.tsx b/src/dev/surfaces/options/OnrampOptions.tsx
index 7233219266..305712aa0e 100644
--- a/src/dev/surfaces/options/OnrampOptions.tsx
+++ b/src/dev/surfaces/options/OnrampOptions.tsx
@@ -40,7 +40,7 @@ function Shell({ children }: { children: React.ReactNode }) {
return (
-
+
{t('title')}
diff --git a/src/features/home/components/HomeActionDrawers.tsx b/src/features/home/components/HomeActionDrawers.tsx
index e660c3b25d..213500cb97 100644
--- a/src/features/home/components/HomeActionDrawers.tsx
+++ b/src/features/home/components/HomeActionDrawers.tsx
@@ -93,7 +93,7 @@ export function HomeActionDrawers() {
return (
!isOpen && setDrawer(null)} hideBottomNav>
-
+
{content && (
{content === 'add' &&
}
diff --git a/src/features/payments/flows/contribute-pot/components/ContributorsDrawer.tsx b/src/features/payments/flows/contribute-pot/components/ContributorsDrawer.tsx
index 8aaf5a3736..5525a22e2d 100644
--- a/src/features/payments/flows/contribute-pot/components/ContributorsDrawer.tsx
+++ b/src/features/payments/flows/contribute-pot/components/ContributorsDrawer.tsx
@@ -59,7 +59,7 @@ export function ContributorsDrawer({ contributors }: ContributorsDrawerProps) {
{t('contributors.title', { count: contributors.length })}
-
+
{contributorCards.map((contributor, index) => (
Date: Mon, 7 Sep 2026 17:01:10 +0530
Subject: [PATCH 2/3] fix(drawer): page-background sheet + honest touch
handling
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Three Kush findings, one commit because they share the drawer chrome:
1. Sheet background back to bg-background-page. e55b5d130 (#2984)
deliberately went white; Kush's 2026-09-07 ruling reverts it — the
sheet matches the app page background. Flagged in the PR body since
it reverses a 3-day-old deliberate change.
2. Removed the Content-level onTouchMove stopPropagation. It silenced
vaul's own document-level touchmove handlers (scroll containment +
drag coordination), which is why dragging the sheet from its body
felt broken and touches seemed to reach the page behind. The reason
it existed — the document-level pull-to-refresh reacting to drawer
touches — is now fixed at the root: usePullToRefresh ignores any
touch starting on [data-vaul-drawer]/[data-vaul-overlay] (with a
regression test). Whole-sheet drag-to-dismiss and no-P2R-during-drag
verified with synthesized CDP touch swipes.
3. scrollbar-none on the drawer scroll container — Android flashes a
scrollbar there while the sheet is dragged (tailwind-scrollbar
utility, same overflow chain as the shadow fix).
---
src/components/Global/Drawer/index.tsx | 17 ++++++++----
src/hooks/__tests__/usePullToRefresh.test.tsx | 26 +++++++++++++++++++
src/hooks/usePullToRefresh.ts | 5 ++++
3 files changed, 43 insertions(+), 5 deletions(-)
diff --git a/src/components/Global/Drawer/index.tsx b/src/components/Global/Drawer/index.tsx
index 4c9eb26714..00d458a305 100644
--- a/src/components/Global/Drawer/index.tsx
+++ b/src/components/Global/Drawer/index.tsx
@@ -103,15 +103,20 @@ const DrawerContent = React.forwardRef e.stopPropagation()}
+ // no onTouchMove stopPropagation here: it silenced vaul's own
+ // document-level touchmove handlers (scroll containment + drag
+ // coordination), which broke dragging the sheet from its body.
+ // pull-to-refresh ignores drawer touches itself (usePullToRefresh).
>
{accessibleTitle && {accessibleTitle}}
@@ -126,7 +131,9 @@ const DrawerContent = React.forwardRef
diff --git a/src/hooks/__tests__/usePullToRefresh.test.tsx b/src/hooks/__tests__/usePullToRefresh.test.tsx
index 618761f482..0fe393352b 100644
--- a/src/hooks/__tests__/usePullToRefresh.test.tsx
+++ b/src/hooks/__tests__/usePullToRefresh.test.tsx
@@ -160,4 +160,30 @@ describe('usePullToRefresh', () => {
expect(invalidateQueries).not.toHaveBeenCalled()
expect(impactHaptic).not.toHaveBeenCalled()
})
+
+ it('ignores touches that start on an open vaul drawer', () => {
+ const invalidateQueries = jest.spyOn(queryClient, 'invalidateQueries')
+ renderHook(() => usePullToRefresh(), { wrapper })
+
+ const sheet = document.createElement('div')
+ sheet.setAttribute('data-vaul-drawer', '')
+ const inner = document.createElement('button')
+ sheet.appendChild(inner)
+ document.body.appendChild(sheet)
+
+ const start = new Event('touchstart', { bubbles: true })
+ Object.defineProperty(start, 'touches', { value: [{ clientX: 0, clientY: 0 }] })
+ act(() => {
+ inner.dispatchEvent(start)
+ })
+
+ // a full downward drag on the sheet must not move the indicator
+ touch('touchmove', 200)
+ touch('touchend', 200)
+
+ expect(indicator()?.style.opacity).toBe('0')
+ expect(invalidateQueries).not.toHaveBeenCalled()
+ expect(impactHaptic).not.toHaveBeenCalled()
+ sheet.remove()
+ })
})
diff --git a/src/hooks/usePullToRefresh.ts b/src/hooks/usePullToRefresh.ts
index 0bf0d2d7df..45d70c94bc 100644
--- a/src/hooks/usePullToRefresh.ts
+++ b/src/hooks/usePullToRefresh.ts
@@ -168,6 +168,11 @@ export const usePullToRefresh = (options: UsePullToRefreshOptions = {}) => {
const onTouchStart = (e: TouchEvent) => {
if (refreshing || e.touches.length !== 1) return
+ // a touch on an open vaul sheet (or its overlay) is a drawer
+ // gesture, never a pull — without this, dragging the sheet moved
+ // the page behind it (the listeners here are on document)
+ const target = e.target as Element | null
+ if (target?.closest?.('[data-vaul-drawer],[data-vaul-overlay]')) return
const allowed = shouldPullToRefreshRef.current ? shouldPullToRefreshRef.current() : window.scrollY === 0
if (!allowed) return
// a new pull can start inside the retract window — put the arrow back
From 4e809631320bc8894e168d436da469f251c4e29f Mon Sep 17 00:00:00 2001
From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com>
Date: Mon, 7 Sep 2026 23:02:04 +0530
Subject: [PATCH 3/3] =?UTF-8?q?revert(drawer):=20keep=20the=20white=20shee?=
=?UTF-8?q?t=20=E2=80=94=20bg=20ruling=20flipped?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Kush's 2026-09-07 ruling reversed: the sheet stays white (Slava's #2984
clean-surface choice), not bg-background-page. Only the background part
of 5422facba reverts — the touch/drag fixes, pull-to-refresh guard, and
scrollbar-none all stay.
---
src/components/Global/Drawer/index.tsx | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/components/Global/Drawer/index.tsx b/src/components/Global/Drawer/index.tsx
index 00d458a305..f74bac7bc3 100644
--- a/src/components/Global/Drawer/index.tsx
+++ b/src/components/Global/Drawer/index.tsx
@@ -103,12 +103,12 @@ const DrawerContent = React.forwardRef