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.
This commit is contained in:
MHSanaei
2026-10-13 22:42:59 +02:00
parent 61885c9126
commit 319e693276
3 changed files with 115 additions and 21 deletions
+30 -21
View File
@@ -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;
}
@@ -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<HTMLElement>(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) => (
<div style={{ width: 360 }}>
<SponsorCard {...args} />
</div>
),
play: async ({ canvasElement }) => {
await expectNotClipped(canvasElement, '.sponsor-title');
},
};
export const SidebarLongCopy: Story = {
args: { variant: 'compact', sponsor: longSponsor, onClose: () => {} },
render: (args) => (
<div style={{ width: 204 }}>
<SponsorCard {...args} />
</div>
),
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) => (
<div style={{ width: 334 }}>
<SponsorCard {...args} />
</div>
),
play: async ({ canvasElement }) => {
await expectNotClipped(canvasElement, '.sponsor-title');
await expectNotClipped(canvasElement, '.sponsor-text');
},
};
export const PageLongCopy: Story = {
args: { variant: 'card', sponsor: longSponsor },
render: (args) => (
<div style={{ width: 320 }}>
<SponsorCard {...args} />
</div>
),
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 },
};
@@ -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}
>
<SponsorLogo sponsor={sponsor} />
<span className="sponsor-body" dir="auto">