From 612c4139afc1c97bd9b637fe721698f5346af483 Mon Sep 17 00:00:00 2001 From: chenhang Date: Tue, 21 Jul 2026 15:15:50 +0800 Subject: [PATCH] feat(tip): refine single-screen payment layout --- .../payment-method-selector.test.tsx | 22 + .../payment/payment-method-selector.tsx | 24 +- .../__tests__/tip-screen.checkout.test.tsx | 84 ++- src/app/tip/tip-screen.module.css | 606 +++++++++--------- src/app/tip/tip-screen.tsx | 209 +++--- 5 files changed, 509 insertions(+), 436 deletions(-) diff --git a/src/app/_components/payment/__tests__/payment-method-selector.test.tsx b/src/app/_components/payment/__tests__/payment-method-selector.test.tsx index 49c8d895..e2e2a56a 100644 --- a/src/app/_components/payment/__tests__/payment-method-selector.test.tsx +++ b/src/app/_components/payment/__tests__/payment-method-selector.test.tsx @@ -62,6 +62,28 @@ describe("PaymentMethodSelector", () => { ); }); + it("renders compact controls without the caption", () => { + const html = renderToStaticMarkup( + undefined} + />, + ); + + expect(html).toContain('data-density="compact"'); + expect(html).toContain("Payment Method"); + expect(html).not.toContain("This caption is hidden"); + expect(html).toContain("min-h-11"); + }); + it("does not render when payment method selection is unavailable", () => { const html = renderToStaticMarkup( -
+

Payment Method

- - {caption} - + {!isCompact ? ( + + {caption} + + ) : null}
{paymentMethods.map((method) => { const selected = method.channel === value; const classes = [ - "flex min-h-[clamp(54px,11.481vw,62px)] cursor-pointer items-center justify-center rounded-(--responsive-card-radius-sm,18px) px-[clamp(12px,2.593vw,14px)] py-[clamp(9px,1.852vw,10px)] text-center font-[inherit] text-[#3c3b3b] transition-[border-color,box-shadow,transform] duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent active:enabled:scale-98 disabled:cursor-not-allowed disabled:opacity-72", + `flex cursor-pointer items-center justify-center text-center font-[inherit] text-[#3c3b3b] transition-[border-color,box-shadow,transform] duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent active:enabled:scale-98 disabled:cursor-not-allowed disabled:opacity-72 ${isCompact ? "min-h-11 rounded-2xl px-2 py-1.5" : "min-h-[clamp(54px,11.481vw,62px)] rounded-(--responsive-card-radius-sm,18px) px-[clamp(12px,2.593vw,14px)] py-[clamp(9px,1.852vw,10px)]"}`, selected ? "border-2 border-[#f657a0] bg-[#fff4f9] shadow-[0_8px_18px_rgba(217,47,127,0.24),0_0_0_3px_rgba(217,47,127,0.18)] -translate-y-0.5" : "border border-[rgba(246,87,160,0.18)] bg-white shadow-[0_5px_7px_0_rgba(247,89,168,0.08)]", @@ -97,7 +105,7 @@ export function PaymentMethodSelector({ aria-hidden="true" width={122} height={29} - className="block max-h-8.5 w-[min(100%,92px)] object-contain" + className={`block object-contain ${isCompact ? "max-h-7 w-[min(100%,80px)]" : "max-h-8.5 w-[min(100%,92px)]"}`} /> ) : ( diff --git a/src/app/tip/__tests__/tip-screen.checkout.test.tsx b/src/app/tip/__tests__/tip-screen.checkout.test.tsx index 47a1fcc9..340ae25b 100644 --- a/src/app/tip/__tests__/tip-screen.checkout.test.tsx +++ b/src/app/tip/__tests__/tip-screen.checkout.test.tsx @@ -20,14 +20,18 @@ const mocks = vi.hoisted(() => ({ })); vi.mock("@/app/_components", () => ({ - BackButton: () => null, - CharacterAvatar: () => null, + BackButton: () => , + CharacterAvatar: ({ alt }: { alt: string }) => ( + {alt} + ), })); vi.mock("@/app/_components/core", () => ({ MobileShell: ({ children }: { children: React.ReactNode }) => children, })); vi.mock("@/app/_components/payment/payment-method-selector", () => ({ - PaymentMethodSelector: () => null, + PaymentMethodSelector: ({ density }: { density?: string }) => ( + + ), })); vi.mock("@/app/_hooks/use-payment-method-selection", () => ({ usePaymentMethodSelection: () => undefined, @@ -85,10 +89,20 @@ vi.mock("../tip-checkout-button", () => ({ ), })); vi.mock("../tip-gift-product-selector", () => ({ - TipGiftProductSelector: () => null, + TipGiftProductSelector: ({ + products, + }: { + products: readonly { planId: string; planName: string }[]; + }) => ( + + {products.map((product) => product.planName).join(",")} + + ), })); vi.mock("../tip-product-image", () => ({ - TipProductImage: ({ alt }: { alt: string }) => {alt}, + TipProductImage: ({ alt }: { alt: string }) => ( + {alt} + ), })); vi.mock("../use-tip-support-prompt", () => ({ useTipSupportPrompt: () => ({ @@ -174,12 +188,30 @@ describe("TipScreen checkout", () => { }); }); + it("renders the compact purchase flow with character atmosphere", () => { + renderScreen(); + + expect(container.textContent).toContain("Send Maya a gift"); + expect(container.textContent).toContain("A warm coffee prompt."); + expect(container.querySelector('[data-testid="tip-back-button"]')).not.toBeNull(); + expect(container.querySelector('[data-testid="tip-character-avatar"]')).not.toBeNull(); + expect(container.querySelector('[data-testid="tip-product-image"]')).not.toBeNull(); + expect(container.querySelector('[data-testid="tip-product-selector"]')).not.toBeNull(); + expect( + container + .querySelector('[data-testid="tip-payment-method"]') + ?.getAttribute("data-density"), + ).toBe("compact"); + expect(container.textContent).not.toContain("A little sweetness for today"); + expect(container.textContent).not.toContain("Tip Maya"); + expect( + container + .querySelector("main") + ?.getAttribute("style"), + ).toContain('--tip-cover-image: url("/images/cover/maya.png")'); + }); + it.each([ - ["catalog is loading", { isLoadingPlans: true }], - [ - "the selected product is missing", - { plans: [], giftProducts: [], selectedPlanId: "" }, - ], ["an order is being created", { isCreatingOrder: true }], ["an order is being polled", { isPollingOrder: true }], ] as const)("disables checkout when %s", (_label, overrides) => { @@ -188,6 +220,38 @@ describe("TipScreen checkout", () => { expect(getCheckoutButton().disabled).toBe(true); }); + it.each([ + ["catalog is loading", { isLoadingPlans: true }], + [ + "the catalog is empty", + { plans: [], giftProducts: [], selectedPlanId: "" }, + ], + ] as const)("hides payment controls when %s", (_label, overrides) => { + mocks.payment = makePaymentState(overrides); + renderScreen(); + + expect(container.querySelector('[data-testid="tip-checkout"]')).toBeNull(); + expect(container.querySelector('[data-testid="tip-payment-method"]')).toBeNull(); + }); + + it("shows a compact retry state when the catalog fails", () => { + mocks.payment = makePaymentState({ + plans: [], + giftProducts: [], + selectedPlanId: "", + errorMessage: "catalog unavailable", + }); + renderScreen(); + + expect(container.textContent).toContain( + "We could not load gifts for this character.", + ); + act(() => getButton("Try again").click()); + expect(mocks.paymentDispatch).toHaveBeenCalledWith({ + type: "PaymentCatalogRetryRequested", + }); + }); + it("shows fixed copy for the first character Tip", () => { mocks.payment = makePaymentState({ status: "paid", diff --git a/src/app/tip/tip-screen.module.css b/src/app/tip/tip-screen.module.css index e50c5c4e..9f7c60be 100644 --- a/src/app/tip/tip-screen.module.css +++ b/src/app/tip/tip-screen.module.css @@ -1,186 +1,162 @@ .shell { position: relative; + display: flex; + width: 100%; + height: var(--app-viewport-height, 100dvh); min-height: var(--app-viewport-height, 100dvh); + box-sizing: border-box; + flex-direction: column; padding: - calc(var(--app-safe-top, 0px) + clamp(18px, 4.444vw, 24px)) - calc(var(--app-safe-right, 0px) + clamp(20px, 5.556vw, 30px)) - calc(var(--app-safe-bottom, 0px) + clamp(24px, 6.667vw, 36px)) - calc(var(--app-safe-left, 0px) + clamp(20px, 5.556vw, 30px)); - overflow: hidden auto; + calc(var(--app-safe-top, 0px) + 14px) + calc(var(--app-safe-right, 0px) + clamp(14px, 4vw, 22px)) + calc(var(--app-safe-bottom, 0px) + 14px) + calc(var(--app-safe-left, 0px) + clamp(14px, 4vw, 22px)); + overflow-x: hidden; + overflow-y: auto; background: - radial-gradient(circle at 18% 12%, rgba(255, 181, 105, 0.34), transparent 32%), - radial-gradient(circle at 86% 24%, rgba(255, 85, 139, 0.2), transparent 30%), - linear-gradient(180deg, #fff2e8 0%, #fff8f2 46%, #ffffff 100%); + radial-gradient(circle at 88% 8%, rgba(255, 116, 151, 0.14), transparent 28%), + linear-gradient(180deg, #fff2e8 0%, #fff9f4 58%, #ffffff 100%); color: #25191b; -} - -.bgImage, -.bgGlowOne, -.bgGlowTwo { - position: absolute; - pointer-events: none; + isolation: isolate; } .bgImage { - inset: 0 0 auto; - height: 330px; - background: - linear-gradient(180deg, rgba(255, 242, 232, 0.3), #fff2e8 96%), - var(--tip-cover-image) center 18% / cover no-repeat; - opacity: 0.36; - filter: saturate(0.95) blur(0.2px); -} - -.bgGlowOne, -.bgGlowTwo { + position: absolute; z-index: 0; - border-radius: 999px; - filter: blur(10px); -} - -.bgGlowOne { - top: 192px; - right: -86px; - width: 190px; - height: 190px; - background: rgba(255, 92, 142, 0.16); -} - -.bgGlowTwo { - bottom: 108px; - left: -98px; - width: 210px; - height: 210px; - background: rgba(255, 181, 104, 0.18); -} - -.header, -.hero, -.productCard, -.paymentMethodSlot, -.statusMessage, -.catalogStatus, -.checkoutSlot { - position: relative; - z-index: 1; + inset: 0 0 auto; + height: clamp(230px, 42vh, 330px); + background: + linear-gradient( + 180deg, + rgba(255, 242, 232, 0.18) 0%, + rgba(255, 246, 239, 0.5) 62%, + #fff9f4 100% + ), + var(--tip-cover-image) center 20% / cover no-repeat; + filter: saturate(0.9); + opacity: 0.48; + pointer-events: none; } .header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; -} - -.headerPill { - display: inline-flex; - align-items: center; - min-height: 34px; - padding: 0 14px; - border: 1px solid rgba(255, 116, 151, 0.22); - border-radius: 999px; - background: rgba(255, 255, 255, 0.66); - color: #9b5360; - font-size: clamp(12px, 2.963vw, 14px); - font-weight: 850; - letter-spacing: 0.03em; - backdrop-filter: blur(18px); -} - -.hero { - display: flex; - flex-direction: column; - align-items: center; - margin-top: clamp(18px, 5.185vw, 28px); - text-align: center; - animation: floatIn 0.5s ease both; -} - -.avatarRing { + position: relative; + z-index: 1; display: grid; - width: clamp(88px, 22.222vw, 112px); - height: clamp(88px, 22.222vw, 112px); - place-items: center; - border: 4px solid rgba(255, 255, 255, 0.9); - border-radius: 9999px; - background: linear-gradient(145deg, #ffbd7c, #ff5d91); - box-shadow: - 0 20px 44px rgba(255, 98, 133, 0.24), - inset 0 0 0 1px rgba(255, 255, 255, 0.32); + min-height: 46px; + grid-template-columns: 42px minmax(0, 1fr) 42px; + align-items: center; + gap: 8px; +} + +.headerIdentity { + display: flex; + min-width: 0; + align-items: center; + justify-content: center; + gap: 10px; +} + +.headerAvatar { + width: 42px; + height: 42px; + flex: 0 0 auto; + border: 2px solid rgba(255, 255, 255, 0.94); + border-radius: 999px; + background: #f3d3c3; + box-shadow: 0 8px 20px rgba(112, 65, 66, 0.16); overflow: hidden; } -.eyebrow { - margin: 16px 0 0; - color: #b35d63; - font-size: clamp(12px, 3.148vw, 15px); - font-weight: 850; - letter-spacing: 0.08em; - text-transform: uppercase; +.headerTitle { + max-width: 220px; + margin: 0; + overflow: hidden; + color: #2a1a1d; + font-family: var(--font-athelas), Georgia, serif; + font-size: clamp(18px, 5vw, 24px); + font-weight: 760; + letter-spacing: -0.025em; + line-height: 1.05; + text-align: left; + text-overflow: ellipsis; + white-space: nowrap; } -.title { - max-width: 320px; - margin: 8px 0 0; - color: #231518; - font-size: clamp(34px, 9.259vw, 48px); - font-weight: 950; - letter-spacing: -0.055em; - line-height: 0.92; +.headerBalance { + display: block; + width: 42px; + height: 1px; } -.subtitle { - min-height: 4.56em; - max-width: 330px; - margin: 14px 0 0; - color: #7c6062; - font-size: clamp(14px, 3.704vw, 18px); - font-weight: 620; - line-height: 1.52; - opacity: 0.78; +.supportPrompt { + position: relative; + z-index: 1; + min-height: 3.7em; + box-sizing: border-box; + margin: 10px 0 0; + padding: 9px 13px; + border: 1px solid rgba(255, 255, 255, 0.64); + border-radius: 18px; + background: rgba(255, 255, 255, 0.7); + color: #684d51; + font-size: clamp(12.5px, 3.45vw, 14px); + font-weight: 650; + line-height: 1.35; + text-align: center; text-wrap: balance; + opacity: 0.78; + box-shadow: 0 10px 26px rgba(91, 55, 52, 0.08); + backdrop-filter: blur(12px); transition: opacity 0.24s ease; } -.subtitleReady { +.supportPromptReady { opacity: 1; } +.purchaseFlow { + position: relative; + z-index: 1; + display: flex; + min-height: 0; + flex: 1 1 auto; + flex-direction: column; + gap: 12px; + margin-top: 12px; +} + .productCard { display: grid; - grid-template-columns: minmax(112px, 0.85fr) 1fr; + flex: 0 1 auto; + grid-template-columns: clamp(98px, 28vw, 118px) minmax(0, 1fr); align-items: center; - gap: clamp(16px, 4.444vw, 24px); - margin-top: clamp(24px, 7.407vw, 38px); - padding: clamp(18px, 5.185vw, 28px); - border: 1px solid rgba(118, 61, 55, 0.08); - border-radius: clamp(30px, 8.148vw, 44px); - background: - linear-gradient(145deg, rgba(255, 255, 255, 0.9), rgba(255, 237, 223, 0.78)), - #ffffff; + gap: 10px; + padding: 12px; + border: 1px solid rgba(118, 61, 55, 0.09); + border-radius: 24px; + background: rgba(255, 255, 255, 0.86); box-shadow: - 0 26px 62px rgba(119, 72, 67, 0.13), - inset 0 1px 0 rgba(255, 255, 255, 0.86); - animation: floatIn 0.5s 0.08s ease both; + 0 16px 36px rgba(119, 72, 67, 0.11), + inset 0 1px 0 rgba(255, 255, 255, 0.9); + backdrop-filter: blur(16px); } .coffeeStage { position: relative; width: 100%; aspect-ratio: 1; + align-self: center; overflow: hidden; - border-radius: clamp(24px, 6.667vw, 32px); + border-radius: 20px; background: #c59b7d; box-shadow: - 0 18px 34px rgba(102, 53, 39, 0.18), + 0 10px 24px rgba(102, 53, 39, 0.16), inset 0 0 0 1px rgba(126, 66, 57, 0.08); - isolation: isolate; } .coffeeStage::after { position: absolute; inset: 0; - z-index: 1; border-radius: inherit; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.34); content: ""; @@ -193,39 +169,6 @@ object-fit: cover; } -.productCopy { - min-width: 0; -} - -.productBadge { - display: inline-flex; - align-items: center; - gap: 7px; - padding: 8px 11px; - border-radius: 999px; - background: rgba(255, 91, 142, 0.1); - color: #b84d65; - font-size: clamp(11px, 2.778vw, 13px); - font-weight: 900; -} - -.productName { - margin: 15px 0 0; - color: #231719; - font-size: clamp(28px, 7.407vw, 40px); - font-weight: 950; - letter-spacing: -0.045em; - line-height: 1; -} - -.productPrice { - margin: 10px 0 0; - color: #ff4f86; - font-size: clamp(25px, 6.667vw, 36px); - font-weight: 950; - letter-spacing: -0.04em; -} - .visuallyHidden { position: absolute; width: 1px; @@ -239,69 +182,55 @@ } .tierSelector { - grid-column: 1 / -1; min-width: 0; - margin: 2px 0 0; + min-height: 0; padding: 0; + margin: 0; border: 0; } -.tierLegend { - width: 100%; - margin-bottom: 12px; - color: #6f5055; - font-size: clamp(13px, 3.148vw, 15px); - font-weight: 850; - letter-spacing: 0.025em; -} - .tierList { display: grid; - gap: 10px; + max-height: 176px; + gap: 6px; + padding: 2px; + overflow-x: hidden; + overflow-y: auto; + overscroll-behavior: contain; + scrollbar-width: thin; } .tierOption { position: relative; display: grid; - grid-template-columns: minmax(0, 1fr) auto 28px; + min-height: 50px; + grid-template-columns: minmax(0, 1fr) auto 20px; align-items: center; - gap: 12px; - min-height: 72px; - padding: 12px 14px 12px 16px; + gap: 7px; + box-sizing: border-box; + padding: 7px 8px 7px 10px; border: 1px solid rgba(112, 71, 65, 0.12); - border-radius: 20px; - background: rgba(255, 255, 255, 0.68); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9); + border-radius: 15px; + background: rgba(255, 255, 255, 0.72); cursor: pointer; transition: - border-color 0.18s ease, - background 0.18s ease, - box-shadow 0.18s ease, - transform 0.18s ease; + border-color 0.16s ease, + background 0.16s ease, + box-shadow 0.16s ease, + transform 0.16s ease; } .tierOption[data-selected="true"] { border-color: var(--color-accent, #f84d96); - background: - linear-gradient(105deg, rgba(255, 255, 255, 0.94), rgba(255, 235, 243, 0.9)), - #ffffff; + background: #fff4f8; box-shadow: - 0 12px 28px rgba(248, 77, 150, 0.16), - inset 3px 0 0 var(--color-accent, #f84d96); + 0 7px 17px rgba(248, 77, 150, 0.13), + inset 2px 0 0 var(--color-accent, #f84d96); } -.tierOption[data-unavailable="true"], .tierSelector:disabled .tierOption { cursor: not-allowed; -} - -.tierOption[data-unavailable="true"] { - filter: grayscale(0.25); - opacity: 0.56; -} - -.tierSelector:disabled .tierOption { - opacity: 0.72; + opacity: 0.68; } .tierInput { @@ -317,8 +246,8 @@ } .tierOption:has(.tierInput:focus-visible) { - outline: 3px solid rgba(248, 77, 150, 0.28); - outline-offset: 3px; + outline: 3px solid rgba(248, 77, 150, 0.26); + outline-offset: 2px; } .tierDetails, @@ -328,79 +257,72 @@ flex-direction: column; } -.tierDetails { - gap: 4px; -} - .tierName { + display: -webkit-box; overflow: hidden; color: #2a1b1e; font-family: var(--font-athelas), Georgia, serif; - font-size: clamp(17px, 4.259vw, 21px); + font-size: clamp(13px, 3.7vw, 16px); font-weight: 750; - letter-spacing: -0.02em; + letter-spacing: -0.015em; line-height: 1.05; - text-overflow: ellipsis; - white-space: nowrap; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; } .tierPriceBlock { align-items: flex-end; - gap: 3px; text-align: right; } .tierPrice { color: #d23f79; - font-size: clamp(14px, 3.519vw, 17px); - font-weight: 950; + font-size: clamp(12px, 3.2vw, 14px); + font-weight: 900; white-space: nowrap; } -.tierUnavailable { - color: #8f7376; - font-size: 10px; - font-weight: 800; -} - .tierCheck { display: grid; - width: 25px; - height: 25px; + width: 19px; + height: 19px; place-items: center; border: 1px solid rgba(111, 76, 73, 0.2); border-radius: 999px; color: transparent; } +.tierCheck svg { + width: 12px; + height: 12px; +} + .tierOption[data-selected="true"] .tierCheck { border-color: var(--color-accent, #f84d96); background: var(--color-accent, #f84d96); color: #ffffff; - box-shadow: 0 5px 14px rgba(248, 77, 150, 0.26); -} - -.statusMessage { - margin: 0; } .catalogStatus { display: grid; + min-height: 180px; + flex: 1 1 auto; + place-content: center; justify-items: center; gap: 12px; - margin-top: 20px; - padding: 22px; + padding: 20px; border: 1px solid rgba(112, 71, 65, 0.1); - border-radius: 24px; + border-radius: 22px; background: rgba(255, 255, 255, 0.78); color: #76575c; font-size: 14px; font-weight: 700; - line-height: 1.5; + line-height: 1.45; text-align: center; } .catalogStatus p { + max-width: 270px; margin: 0; } @@ -418,11 +340,11 @@ } .productSkeleton { - min-height: 260px; + min-height: 174px; } .skeletonImage, -.skeletonCopy span { +.skeletonList span { background: linear-gradient( 100deg, rgba(205, 172, 157, 0.14) 20%, @@ -436,50 +358,32 @@ .skeletonImage { width: 100%; aspect-ratio: 1; - border-radius: clamp(24px, 6.667vw, 32px); + border-radius: 20px; } -.skeletonCopy { +.skeletonList { display: grid; - gap: 12px; + gap: 6px; } -.skeletonCopy span { +.skeletonList span { display: block; - height: 18px; - border-radius: 999px; -} - -.skeletonCopy span:nth-child(2) { - width: 78%; - height: 38px; -} - -.skeletonCopy span:nth-child(3) { - width: 56%; - height: 28px; -} - -.statusMessage { - margin-top: 14px; - color: #b2474f; - font-size: 14px; - font-weight: 720; - line-height: 1.45; - text-align: center; + min-height: 50px; + border-radius: 15px; } .paymentMethodSlot { - margin-top: clamp(18px, 5.185vw, 28px); + flex: 0 0 auto; } .checkoutSlot { - margin-top: clamp(18px, 5.185vw, 28px); + flex: 0 0 auto; + margin-top: auto; } .checkoutButton { width: 100%; - min-height: 58px; + min-height: 52px; border: 0; border-radius: 999px; background: @@ -488,12 +392,12 @@ color: #ffffff; cursor: pointer; font: inherit; - font-size: clamp(16px, 4.074vw, 19px); - font-weight: 940; + font-size: clamp(15px, 4vw, 18px); + font-weight: 900; letter-spacing: -0.01em; box-shadow: - 0 18px 40px rgba(255, 83, 137, 0.28), - 0 8px 18px rgba(37, 23, 27, 0.18); + 0 14px 30px rgba(255, 83, 137, 0.24), + 0 6px 14px rgba(37, 23, 27, 0.15); transition: transform 0.18s ease, filter 0.18s ease, box-shadow 0.18s ease; } @@ -501,7 +405,7 @@ cursor: not-allowed; filter: grayscale(0.25); opacity: 0.62; - box-shadow: 0 10px 24px rgba(77, 52, 55, 0.12); + box-shadow: 0 8px 20px rgba(77, 52, 55, 0.1); } .checkoutButton:not(:disabled):active { @@ -509,51 +413,146 @@ } .checkoutError { - margin: 10px 0 0; + margin: 7px 0 0; color: #b2474f; - font-size: 14px; - font-weight: 720; - line-height: 1.45; + font-size: 12px; + font-weight: 700; + line-height: 1.35; text-align: center; } -@media (max-width: 380px) { - .productCard { - grid-template-columns: 1fr; - text-align: center; +@media (max-width: 350px) { + .shell { + padding-right: calc(var(--app-safe-right, 0px) + 11px); + padding-left: calc(var(--app-safe-left, 0px) + 11px); } - .coffeeStage { - width: min(100%, 220px); - justify-self: center; + .header { + grid-template-columns: 40px minmax(0, 1fr) 40px; + gap: 5px; + } + + .headerIdentity { + gap: 7px; + } + + .headerTitle { + max-width: 170px; + font-size: 17px; + } + + .productCard { + grid-template-columns: 82px minmax(0, 1fr); + gap: 7px; + padding: 9px; + } + + .tierOption { + grid-template-columns: minmax(0, 1fr) auto 18px; + gap: 5px; + padding-right: 6px; + padding-left: 8px; } .tierName { - white-space: normal; + font-size: 12px; + } + + .tierPrice { + font-size: 11px; + } + + .tierCheck { + width: 17px; + height: 17px; + } +} + +@media (max-height: 700px) { + .shell { + padding-top: calc(var(--app-safe-top, 0px) + 9px); + padding-bottom: calc(var(--app-safe-bottom, 0px) + 9px); + } + + .header { + min-height: 40px; + } + + .headerAvatar { + width: 38px; + height: 38px; + } + + .purchaseFlow { + gap: 8px; + margin-top: 8px; + } + + .supportPrompt { + min-height: 0; + margin-top: 7px; + padding: 7px 11px; + font-size: 12px; + line-height: 1.3; + } + + .productCard { + grid-template-columns: 92px minmax(0, 1fr); + padding: 9px; + } + + .tierList { + max-height: 148px; + gap: 5px; + } + + .tierOption, + .skeletonList span { + min-height: 44px; + } + + .checkoutButton { + min-height: 48px; + } +} + +@media (max-height: 600px) { + .supportPrompt { + display: -webkit-box; + overflow: hidden; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + } + + .productCard { + grid-template-columns: minmax(0, 1fr); + } + + .coffeeStage, + .skeletonImage { + display: none; + } + + .productSkeleton { + min-height: 150px; + } + + .tierList { + max-height: 146px; } } @media (hover: hover) { - .checkoutButton:not(:disabled):hover { - transform: translateY(-1px); - } - - .tierSelector:not(:disabled) - .tierOption:not([data-unavailable="true"]):hover { - border-color: rgba(248, 77, 150, 0.42); + .checkoutButton:not(:disabled):hover, + .tierSelector:not(:disabled) .tierOption:hover { transform: translateY(-1px); } } -@keyframes floatIn { - from { - opacity: 0; - transform: translateY(18px) scale(0.98); - } - - to { - opacity: 1; - transform: translateY(0) scale(1); +@media (prefers-reduced-motion: reduce) { + .skeletonImage, + .skeletonList span { + animation: none; } } @@ -562,10 +561,3 @@ background-position: -120% 0; } } - -@media (prefers-reduced-motion: reduce) { - .skeletonImage, - .skeletonCopy span { - animation: none; - } -} diff --git a/src/app/tip/tip-screen.tsx b/src/app/tip/tip-screen.tsx index 57d799c5..eb6b27a4 100644 --- a/src/app/tip/tip-screen.tsx +++ b/src/app/tip/tip-screen.tsx @@ -1,7 +1,6 @@ "use client"; import { useEffect, type CSSProperties } from "react"; -import { Sparkles } from "lucide-react"; import { BackButton, CharacterAvatar } from "@/app/_components"; import { MobileShell } from "@/app/_components/core"; @@ -25,10 +24,7 @@ import { useUserState } from "@/stores/user/user-context"; import { TipCheckoutButton } from "./tip-checkout-button"; import { TipGiftProductSelector } from "./tip-gift-product-selector"; import { TipProductImage } from "./tip-product-image"; -import { - formatGiftPrice, - getGiftImageSources, -} from "./tip-screen.helpers"; +import { getGiftImageSources } from "./tip-screen.helpers"; import { TipSuccessView } from "./tip-success-view"; import { useTipSupportPrompt } from "./use-tip-support-prompt"; import styles from "./tip-screen.module.css"; @@ -214,8 +210,6 @@ export function TipScreen({ } >