On a Centra fashion PDP, out-of-stock is not a greyed-out add-to-cart. The shopper already picked a size. They need a different primary action, not a dead button and not a second mystery icon in the chip row. I stopped bolting mail icons onto unavailable sizes and started deriving the whole purchase row from stock state.
Purchase helpers now split cleanly. canAddToCart requires a selected size that is available. canNotifyForSize is true when a size is selected and unavailable. resolveSizeOptionState exposes unavailableSelected so chips stay tappable. resolvePrimaryPurchaseLabel returns Choose size, Notify me, or Add to cart. Icon follows: mail vs cart. Unavailable sizes remain selectable so notify can target the size the shopper actually picked.
The notify overlay is a shadcn Sheet: right on desktop, bottom on mobile. Email field, optional size select inside. When the overlay opens, email and variant reset via a render-time prevOpen guard, not a useEffect that fires one frame late. Submit currently closes the sheet; backend wiring was out of scope for this slice. Same flow on PDP, sticky cart, Quick Add, and product-card quick add.
function resolvePrimaryPurchaseLabel(state: PurchaseUiState): string {
if (!state.selectedSize) return 'Choose size';
if (state.notifyMode) return 'Notify me';
return 'Add to cart';
}Then came the shared Panel molecule so every sheet stopped restyling header padding. Engine is 'sheet' | 'drawer'. Placement is 'responsive' (md+ right, else bottom) or a fixed side. Compound parts (Trigger, Content, Header, Body, Footer, Title, Description, Close) pick Drawer or Sheet primitives from context. PanelBody is the scroll region: min-h-0 overflow-y-auto. Size guide, notify overlay, cart, filters, mobile nav, and quick add all moved onto it.
Vaul Drawer and shadcn Sheet look interchangeable in a mock. In production they differ in focus trap, width tokens, and which side animates from the viewport edge. One Panel wrapper means I fix those once.
Primary CTA from stock + shared Panel
Pick a size. Out-of-stock M flips the primary button to Notify me and opens a bottom or right Panel overlay.
No size yet. Primary stays Choose size.
Pick OOS size M. Primary flips to Notify me and opens the fake Panel. Switch placement to see bottom sheet vs right drawer.
Model the CTA from stock. Then give every overlay the same shell.
Happy coding! Sander