From 319e693276214b7726432b6b1adec5fbcb0bddb3 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Tue, 13 Oct 2026 22:42:59 +0200 Subject: [PATCH] fix(sponsor): keep long sponsor copy inside every slot The Sponsors page card sized its body to max-content because the title never wrapped, so a long title spilled out of the card onto the next one. The compact card (sidebar, login) put logo, copy and close button in one row, leaving the copy ~110px of the 220px sidebar and clamping it to two lines; the dashboard banner cut its title to a single line. Titles and descriptions now wrap, breaking unbroken strings too. The compact card stacks logo and tag above copy that spans the full card width, keeping a bounded height in the sidebar with a 2-line title and 4-line description clamp and the full copy in the link tooltip. --- .../src/components/sponsor/SponsorCard.css | 51 ++++++----- .../sponsor/SponsorCard.stories.tsx | 84 +++++++++++++++++++ .../src/components/sponsor/SponsorCard.tsx | 1 + 3 files changed, 115 insertions(+), 21 deletions(-) diff --git a/frontend/src/components/sponsor/SponsorCard.css b/frontend/src/components/sponsor/SponsorCard.css index 1fba8f486..caa9fbfe7 100644 --- a/frontend/src/components/sponsor/SponsorCard.css +++ b/frontend/src/components/sponsor/SponsorCard.css @@ -61,7 +61,7 @@ .sponsor-head { display: flex; - align-items: center; + align-items: baseline; gap: 8px; min-width: 0; } @@ -77,15 +77,14 @@ } .sponsor-title { - overflow: hidden; font-weight: 600; - white-space: nowrap; - text-overflow: ellipsis; + overflow-wrap: anywhere; } .sponsor-text { font-size: 13px; color: var(--ant-color-text-secondary); + overflow-wrap: anywhere; } .sponsor-visit { @@ -129,42 +128,52 @@ } .sponsor-card-compact .sponsor-main { - gap: 10px; + display: grid; + grid-template-columns: auto minmax(0, 1fr); + align-items: center; + gap: 4px 8px; padding: 8px 10px; } -.sponsor-card-compact .sponsor-logo { - width: 32px; - height: 32px; +.sponsor-card-compact .sponsor-body, +.sponsor-card-compact .sponsor-head { + display: contents; } -.sponsor-card-compact .sponsor-head { - flex-direction: column; - align-items: flex-start; - gap: 2px; +.sponsor-card-compact .sponsor-logo { + width: 28px; + height: 28px; +} + +.sponsor-card-compact .sponsor-tag { + justify-self: start; +} + +.sponsor-card-compact .sponsor-title, +.sponsor-card-compact .sponsor-text { + display: -webkit-box; + grid-column: 1 / -1; + overflow: hidden; + -webkit-box-orient: vertical; } .sponsor-card-compact .sponsor-title { - display: -webkit-box; - max-width: 100%; font-size: 13px; - white-space: normal; -webkit-line-clamp: 2; - -webkit-box-orient: vertical; } .sponsor-card-compact .sponsor-text { - display: -webkit-box; - overflow: hidden; font-size: 12px; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; + -webkit-line-clamp: 4; } .sponsor-card-compact .sponsor-close { + position: absolute; + top: 4px; + inset-inline-end: 4px; width: 22px; height: 22px; - margin: 4px 4px 0 0; + margin: 0; font-size: 11px; } diff --git a/frontend/src/components/sponsor/SponsorCard.stories.tsx b/frontend/src/components/sponsor/SponsorCard.stories.tsx index d12fdacab..b02f2ff2c 100644 --- a/frontend/src/components/sponsor/SponsorCard.stories.tsx +++ b/frontend/src/components/sponsor/SponsorCard.stories.tsx @@ -14,6 +14,34 @@ const sponsor: Sponsor = { link: 'https://acme.example/?utm_source=3x-ui', }; +// Copy as long as the real sponsors.json entries; each story renders it at its slot's width. +const longSponsor: Sponsor = { + ...sponsor, + title: { en: 'Advertise here — reach 3X-UI server admins' }, + text: { + en: 'Put your VPS, hosting or domain brand in front of the admins who use 3X-UI every day. Message us for slots and pricing.', + }, +}; + +function part(root: HTMLElement, selector: string): HTMLElement { + const el = root.querySelector(selector); + if (!el) throw new Error(`missing ${selector}`); + return el; +} + +async function expectInsideCard(root: HTMLElement, selector: string) { + const card = part(root, '.sponsor-card').getBoundingClientRect(); + const box = part(root, selector).getBoundingClientRect(); + await expect(box.left).toBeGreaterThanOrEqual(card.left - 0.5); + await expect(box.right).toBeLessThanOrEqual(card.right + 0.5); +} + +async function expectNotClipped(root: HTMLElement, selector: string) { + const el = part(root, selector); + await expect(el.scrollWidth).toBeLessThanOrEqual(el.clientWidth + 1); + await expect(el.scrollHeight).toBeLessThanOrEqual(el.clientHeight + 1); +} + const meta = { title: 'Sponsor/SponsorCard', component: SponsorCard, @@ -68,6 +96,62 @@ export const Card: Story = { ), }; +export const BannerLongCopy: Story = { + args: { variant: 'banner', sponsor: longSponsor, onClose: () => {} }, + render: (args) => ( +
+ +
+ ), + play: async ({ canvasElement }) => { + await expectNotClipped(canvasElement, '.sponsor-title'); + }, +}; + +export const SidebarLongCopy: Story = { + args: { variant: 'compact', sponsor: longSponsor, onClose: () => {} }, + render: (args) => ( +
+ +
+ ), + play: async ({ canvasElement }) => { + await expectNotClipped(canvasElement, '.sponsor-title'); + await expectInsideCard(canvasElement, '.sponsor-text'); + await expect(part(canvasElement, '.sponsor-main')).toHaveAttribute( + 'title', + `${longSponsor.title.en}\n${longSponsor.text.en}`, + ); + }, +}; + +export const LoginLongCopy: Story = { + args: { variant: 'compact', sponsor: longSponsor, onClose: () => {} }, + render: (args) => ( +
+ +
+ ), + play: async ({ canvasElement }) => { + await expectNotClipped(canvasElement, '.sponsor-title'); + await expectNotClipped(canvasElement, '.sponsor-text'); + }, +}; + +export const PageLongCopy: Story = { + args: { variant: 'card', sponsor: longSponsor }, + render: (args) => ( +
+ +
+ ), + play: async ({ canvasElement }) => { + await expectInsideCard(canvasElement, '.sponsor-title'); + await expectInsideCard(canvasElement, '.sponsor-text'); + await expectNotClipped(canvasElement, '.sponsor-title'); + }, +}; + export const IconOnly: Story = { args: { iconOnly: true }, }; diff --git a/frontend/src/components/sponsor/SponsorCard.tsx b/frontend/src/components/sponsor/SponsorCard.tsx index 015ba3333..957fef224 100644 --- a/frontend/src/components/sponsor/SponsorCard.tsx +++ b/frontend/src/components/sponsor/SponsorCard.tsx @@ -70,6 +70,7 @@ export default function SponsorCard({ href={sponsor.link} target="_blank" rel="noopener noreferrer sponsored" + title={variant === 'compact' ? [title, text].filter(Boolean).join('\n') : undefined} >