mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
fix(web): move active promotions into a top bar
This commit is contained in:
committed by
Vyctor H. Brzezowski
parent
3348b0baa4
commit
2ce2ecf358
@@ -111,9 +111,7 @@ describe("restored UI design contract", () => {
|
||||
]) {
|
||||
expect(cssRule(css, selector)).toContain("border-radius: var(--oc-radius-control)");
|
||||
}
|
||||
expect(cssRule(css, ".home-v2-promotion-title-icon")).toContain(
|
||||
"border-radius: var(--oc-radius-inset)",
|
||||
);
|
||||
expect(cssRule(css, ".promotion-bar-icon")).toContain("border-radius: var(--oc-radius-inset)");
|
||||
expect(cssRule(css, ".home-v2-apps-workflow-tile")).toContain(
|
||||
"border-radius: var(--oc-radius-surface)",
|
||||
);
|
||||
@@ -474,12 +472,15 @@ describe("restored UI design contract", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps promotion cards within narrow mobile viewports", () => {
|
||||
it("keeps the promotion bar thin and horizontally bounded", () => {
|
||||
const rootSource = rootRoute();
|
||||
const css = styles();
|
||||
|
||||
expect(cssRule(css, ".home-v2-promotions")).toContain("padding: 0 48px 56px");
|
||||
expect(cssRule(css, ".home-v2-promotions-track")).toContain("display: grid");
|
||||
expect(css).toContain("padding: 0 20px 48px");
|
||||
expect(rootSource.indexOf("<PromotionsBar />")).toBeLessThan(rootSource.indexOf("<Header />"));
|
||||
expect(cssRule(css, ".promotion-bar-track")).toContain("min-height: 36px");
|
||||
expect(cssRule(css, ".promotion-bar-track")).toContain("overflow-x: auto");
|
||||
expect(cssRule(css, ".promotion-bar-item")).toContain("min-width: min(100%, 520px)");
|
||||
expect(css).toContain("min-width: 100%");
|
||||
});
|
||||
|
||||
it("keeps typeahead creator avatars round for users and square for orgs", () => {
|
||||
|
||||
+24
-4
@@ -1,7 +1,7 @@
|
||||
/* @vitest-environment jsdom */
|
||||
import { act, render, screen } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { HomePromotionsSection } from "./HomePromotionsSection";
|
||||
import { PromotionsBar } from "./PromotionsBar";
|
||||
|
||||
const { fetchMock, publicApiUrlMock } = vi.hoisted(() => ({
|
||||
fetchMock: vi.fn(),
|
||||
@@ -36,7 +36,7 @@ function promotionsResponse(promotions: Array<typeof promotion>) {
|
||||
});
|
||||
}
|
||||
|
||||
describe("HomePromotionsSection", () => {
|
||||
describe("PromotionsBar", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(100_000);
|
||||
@@ -58,7 +58,7 @@ describe("HomePromotionsSection", () => {
|
||||
promotionsResponse([{ ...promotion, startsAt: 120_000, endsAt: 200_000 }]),
|
||||
);
|
||||
|
||||
const { container } = render(<HomePromotionsSection />);
|
||||
const { container } = render(<PromotionsBar />);
|
||||
await flushPromises();
|
||||
expect(screen.queryByText(promotion.title)).toBeNull();
|
||||
expect(fetchMock.mock.calls[0]?.[0]).toBe("https://clawhub.test/api/v1/promotions");
|
||||
@@ -71,6 +71,7 @@ describe("HomePromotionsSection", () => {
|
||||
expect(fetchMock.mock.calls[1]?.[0]).toBe("https://clawhub.test/api/v1/promotions");
|
||||
expect(screen.getByText(promotion.title)).toBeTruthy();
|
||||
expect(screen.getByText(promotion.blurb)).toBeTruthy();
|
||||
expect(screen.queryByText("1 day left")).toBeNull();
|
||||
expect(container.querySelector('img[src="/tencent-hy-favicon.png"]')).toBeNull();
|
||||
});
|
||||
|
||||
@@ -79,7 +80,7 @@ describe("HomePromotionsSection", () => {
|
||||
.mockResolvedValueOnce(promotionsResponse([{ ...promotion, endsAt: 100_500 }]))
|
||||
.mockResolvedValueOnce(promotionsResponse([]));
|
||||
|
||||
render(<HomePromotionsSection />);
|
||||
render(<PromotionsBar />);
|
||||
await flushPromises();
|
||||
expect(screen.getByText(promotion.title)).toBeTruthy();
|
||||
expect(screen.getByText(promotion.blurb)).toBeTruthy();
|
||||
@@ -91,4 +92,23 @@ describe("HomePromotionsSection", () => {
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||
expect(screen.queryByText(promotion.title)).toBeNull();
|
||||
});
|
||||
|
||||
it("uses the campaign date instead of a changing countdown for Tencent Hy3", async () => {
|
||||
fetchMock.mockResolvedValueOnce(
|
||||
promotionsResponse([
|
||||
{
|
||||
...promotion,
|
||||
title: "Tencent Hy3 is free on OpenRouter",
|
||||
endsAt: Date.UTC(2026, 6, 21),
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const { container } = render(<PromotionsBar />);
|
||||
await flushPromises();
|
||||
|
||||
expect(screen.getByText("Tencent's latest model, free until July 21")).toBeTruthy();
|
||||
expect(screen.queryByText(/days left/)).toBeNull();
|
||||
expect(container.querySelector('img[src="/tencent-hy-favicon.png"]')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,6 @@ type PublicPromotion = {
|
||||
slug: string;
|
||||
title: string;
|
||||
blurb: string;
|
||||
sponsor?: string;
|
||||
endsAt: number;
|
||||
signupUrl?: string;
|
||||
docsUrl?: string;
|
||||
@@ -15,7 +14,6 @@ type PublicPromotion = {
|
||||
};
|
||||
|
||||
const PROMOTIONS_POLL_INTERVAL_MS = 60_000;
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
function nextPromotionsRefreshDelay(promotions: PublicPromotion[], now: number) {
|
||||
return promotions.reduce(
|
||||
@@ -33,96 +31,59 @@ function formatPromotionDate(endsAt: number) {
|
||||
}).format(new Date(endsAt));
|
||||
}
|
||||
|
||||
function formatDaysRemaining(endsAt: number) {
|
||||
const days = Math.max(0, Math.ceil((endsAt - Date.now()) / DAY_MS));
|
||||
return `${days} ${days === 1 ? "day" : "days"} left`;
|
||||
}
|
||||
|
||||
function isTencentHyPromotion(title: string) {
|
||||
return /tencent hy3/i.test(title);
|
||||
}
|
||||
|
||||
function promotionDisplayTitle(title: string) {
|
||||
const match = /^(.*?)\s+is free on\s+(.*?)$/i.exec(title);
|
||||
if (!match) {
|
||||
return <span className="home-v2-promotion-title-brand">{title}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className="home-v2-promotion-title-brand">{match[1]}</span>
|
||||
<span className="home-v2-promotion-title-muted"> is free on </span>
|
||||
<span className="home-v2-promotion-title-muted">{match[2]}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function promotionCtaUrl(promotion: PublicPromotion) {
|
||||
return promotion.launchPageUrl ?? promotion.signupUrl ?? promotion.docsUrl ?? null;
|
||||
}
|
||||
|
||||
function promotionMetaCopy(promotion: PublicPromotion) {
|
||||
if (isTencentHyPromotion(promotion.title)) {
|
||||
return `Tencent's latest model, free until ${formatPromotionDate(promotion.endsAt)}.`;
|
||||
return `Tencent's latest model, free until ${formatPromotionDate(promotion.endsAt)}`;
|
||||
}
|
||||
|
||||
return promotion.blurb;
|
||||
}
|
||||
|
||||
function PromotionCard({ promotion }: { promotion: PublicPromotion }) {
|
||||
function PromotionBarItem({ promotion }: { promotion: PublicPromotion }) {
|
||||
const ctaUrl = promotionCtaUrl(promotion);
|
||||
const daysRemainingLabel = formatDaysRemaining(promotion.endsAt);
|
||||
const isTencentPromotion = isTencentHyPromotion(promotion.title);
|
||||
|
||||
return (
|
||||
<article className="home-v2-promotion-card oc-card">
|
||||
<div className="home-v2-promotion-content">
|
||||
<div className="home-v2-promotion-stack">
|
||||
<h3 className="home-v2-promotion-title">
|
||||
{isTencentPromotion ? (
|
||||
<img
|
||||
src="/tencent-hy-favicon.png"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="home-v2-promotion-title-icon"
|
||||
/>
|
||||
) : (
|
||||
<Gift
|
||||
size={20}
|
||||
aria-hidden="true"
|
||||
className="home-v2-promotion-title-icon home-v2-promotion-title-icon-fallback"
|
||||
/>
|
||||
)}
|
||||
<span className="home-v2-promotion-title-copy">
|
||||
{promotionDisplayTitle(promotion.title)}
|
||||
</span>
|
||||
</h3>
|
||||
<p className="home-v2-promotion-meta">{promotionMetaCopy(promotion)}</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* No CLI claim snippet yet: the openclaw `promos claim` command ships
|
||||
separately; advertise it here once that CLI flow exists. */}
|
||||
<div className="home-v2-promotion-actions">
|
||||
<span className="home-v2-promotion-days">
|
||||
<Gift size={14} aria-hidden="true" />
|
||||
<span>{daysRemainingLabel}</span>
|
||||
</span>
|
||||
{ctaUrl ? (
|
||||
<a
|
||||
className="home-v2-promotion-link oc-action oc-action-primary"
|
||||
href={ctaUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Try it free <ArrowUpRight size={15} aria-hidden="true" />
|
||||
</a>
|
||||
) : null}
|
||||
<article className="promotion-bar-item">
|
||||
<div className="promotion-bar-copy">
|
||||
<h3 className="promotion-bar-title">
|
||||
{isTencentPromotion ? (
|
||||
<img
|
||||
src="/tencent-hy-favicon.png"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="promotion-bar-icon"
|
||||
/>
|
||||
) : (
|
||||
<Gift
|
||||
size={14}
|
||||
aria-hidden="true"
|
||||
className="promotion-bar-icon promotion-bar-icon-fallback"
|
||||
/>
|
||||
)}
|
||||
<span className="promotion-bar-title-copy">{promotion.title}</span>
|
||||
</h3>
|
||||
<span className="promotion-bar-separator" aria-hidden="true" />
|
||||
<span className="promotion-bar-meta">{promotionMetaCopy(promotion)}</span>
|
||||
</div>
|
||||
{ctaUrl ? (
|
||||
<a className="promotion-bar-link" href={ctaUrl} target="_blank" rel="noopener noreferrer">
|
||||
Try it free <ArrowUpRight size={15} aria-hidden="true" />
|
||||
</a>
|
||||
) : null}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
export function HomePromotionsSection() {
|
||||
export function PromotionsBar() {
|
||||
const [promotions, setPromotions] = useState<PublicPromotion[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -152,7 +113,7 @@ export function HomePromotionsSection() {
|
||||
setPromotions(active);
|
||||
scheduleRefresh(active);
|
||||
} catch {
|
||||
// Promotions are decorative on the homepage; render nothing on failure.
|
||||
// Promotions are optional header content; render nothing on failure.
|
||||
if (!cancelled) scheduleRefresh([]);
|
||||
}
|
||||
}
|
||||
@@ -167,13 +128,13 @@ export function HomePromotionsSection() {
|
||||
if (promotions.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section className="home-v2-promotions oc-section" aria-labelledby="home-promotions-title">
|
||||
<h2 id="home-promotions-title" className="sr-only">
|
||||
<section className="promotion-bar" aria-labelledby="active-promotions-title">
|
||||
<h2 id="active-promotions-title" className="sr-only">
|
||||
Active promotions
|
||||
</h2>
|
||||
<div className="home-v2-promotions-track">
|
||||
<div className="promotion-bar-track">
|
||||
{promotions.map((promotion) => (
|
||||
<PromotionCard key={promotion.slug} promotion={promotion} />
|
||||
<PromotionBarItem key={promotion.slug} promotion={promotion} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
@@ -17,6 +17,7 @@ import { ErrorBoundary } from "../components/ErrorBoundary";
|
||||
import { Footer } from "../components/Footer";
|
||||
import { GenericNotFoundPage } from "../components/GenericNotFoundPage";
|
||||
import Header from "../components/Header";
|
||||
import { PromotionsBar } from "../components/PromotionsBar";
|
||||
import {
|
||||
BANNED_ACCOUNT_PATH,
|
||||
isBannedAccountAuthError,
|
||||
@@ -194,6 +195,7 @@ function RootDocument({ children }: { children: React.ReactNode }) {
|
||||
<body>
|
||||
<AppProviders>
|
||||
<div className="app-shell">
|
||||
<PromotionsBar />
|
||||
<Header />
|
||||
<ClientOnly>
|
||||
<DeploymentDriftBanner />
|
||||
|
||||
@@ -4,7 +4,6 @@ import { HomeAppsSection } from "../components/HomeAppsSection";
|
||||
import { HomeBringSkillsSection } from "../components/HomeBringSkillsSection";
|
||||
import { HomeListingSection } from "../components/HomeListingSection";
|
||||
import { HomePopularPublishersSection } from "../components/HomePopularPublishersSection";
|
||||
import { HomePromotionsSection } from "../components/HomePromotionsSection";
|
||||
import { HomeV2FoldBottomFade } from "../components/HomeV2FoldBottomFade";
|
||||
import { fetchInitialHomeListing, type HomeListingInitialData } from "../lib/homeListingData";
|
||||
|
||||
@@ -377,7 +376,6 @@ function SkillsHome() {
|
||||
<p className="home-v2-sub oc-hero-lede">Discover skills and plugins from top creators</p>
|
||||
</section>
|
||||
|
||||
<HomePromotionsSection />
|
||||
<HomeListingSection initialListing={initialListing} />
|
||||
<HomePopularPublishersSection />
|
||||
<HomeAppsSection />
|
||||
|
||||
+142
-266
@@ -25543,309 +25543,185 @@ a.search-empty-action {
|
||||
0 1px 2px rgba(15, 23, 42, 0.12);
|
||||
}
|
||||
|
||||
/* ═══ HOME PROMOTIONS ═══ */
|
||||
.home-v2-promotions {
|
||||
width: 100%;
|
||||
max-width: 1296px;
|
||||
margin: 0 auto;
|
||||
padding: 0 48px 56px;
|
||||
}
|
||||
|
||||
.home-v2-promotions-track {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.home-v2-promotion-card {
|
||||
/* ═══ PROMOTION BAR ═══ */
|
||||
.promotion-bar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 32px;
|
||||
min-height: 124px;
|
||||
padding: 28px 30px;
|
||||
z-index: 11;
|
||||
isolation: isolate;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
border: 1px solid color-mix(in srgb, var(--hv2-text) 7%, transparent);
|
||||
border-radius: var(--hv2-radius-md);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.42);
|
||||
background: color-mix(in srgb, var(--hv2-text) 4%, transparent);
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--accent) 26%, var(--line));
|
||||
background:
|
||||
radial-gradient(
|
||||
ellipse 28% 180% at 12% -90%,
|
||||
color-mix(in srgb, var(--accent) 26%, transparent) 0%,
|
||||
transparent 72%
|
||||
),
|
||||
radial-gradient(
|
||||
ellipse 34% 220% at 84% 180%,
|
||||
color-mix(in srgb, var(--accent) 22%, transparent) 0%,
|
||||
transparent 70%
|
||||
),
|
||||
linear-gradient(
|
||||
90deg,
|
||||
var(--surface) 0%,
|
||||
color-mix(in srgb, var(--accent) 9%, var(--surface)) 48%,
|
||||
color-mix(in srgb, var(--accent) 5%, var(--surface)) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.home-v2-promotion-card::before,
|
||||
.home-v2-promotion-card::after {
|
||||
.promotion-bar::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.home-v2-promotion-card::before {
|
||||
background:
|
||||
radial-gradient(
|
||||
ellipse 56% 72% at 26% -8%,
|
||||
rgba(255, 255, 255, 0.14) 0%,
|
||||
rgba(255, 255, 255, 0.06) 38%,
|
||||
rgba(255, 255, 255, 0) 72%
|
||||
),
|
||||
radial-gradient(
|
||||
ellipse 82% 120% at 84% 140%,
|
||||
rgba(204, 48, 24, 0.2) 0%,
|
||||
rgba(244, 68, 46, 0.1) 44%,
|
||||
rgba(244, 68, 46, 0) 76%
|
||||
),
|
||||
radial-gradient(
|
||||
ellipse 36% 70% at 98% 126%,
|
||||
rgba(255, 80, 48, 0.14) 0%,
|
||||
rgba(244, 68, 46, 0) 70%
|
||||
);
|
||||
mix-blend-mode: multiply;
|
||||
z-index: 0;
|
||||
.promotion-bar::before {
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
background: repeating-linear-gradient(
|
||||
105deg,
|
||||
transparent 0 24px,
|
||||
color-mix(in srgb, var(--accent) 12%, transparent) 24px 25px
|
||||
);
|
||||
opacity: 0.5;
|
||||
mask-image: linear-gradient(90deg, transparent 0%, #000 18%, #000 82%, transparent 100%);
|
||||
}
|
||||
|
||||
.home-v2-promotion-card::after {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='80' height='80' viewBox='0 0 80 80'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='80' height='80' filter='url(%23n)' opacity='.5'/%3E%3C/svg%3E");
|
||||
opacity: 0.025;
|
||||
mix-blend-mode: overlay;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.home-v2-promotion-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
.promotion-bar-track {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.home-v2-promotion-stack {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.home-v2-promotion-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
color: var(--hv2-text);
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.home-v2-promotion-title-brand {
|
||||
color: var(--hv2-text);
|
||||
}
|
||||
|
||||
.home-v2-promotion-title-muted {
|
||||
color: var(--hv2-text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.home-v2-promotion-title-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: var(--oc-radius-inset);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.home-v2-promotion-title-icon-fallback {
|
||||
color: var(--hv2-text-secondary);
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.home-v2-promotion-title-copy {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.home-v2-promotion-meta {
|
||||
margin: 0;
|
||||
padding-left: 28px;
|
||||
color: var(--hv2-text-secondary);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.home-v2-promotion-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
flex: 0 0 auto;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
max-width: var(--page-max);
|
||||
min-height: 36px;
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--space-5);
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
scroll-snap-type: x mandatory;
|
||||
}
|
||||
|
||||
.home-v2-promotion-days {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 9ch;
|
||||
color: var(--hv2-text-secondary);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
.promotion-bar-track::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.home-v2-promotion-days svg {
|
||||
color: currentColor;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.home-v2-promotion-link {
|
||||
display: inline-flex;
|
||||
.promotion-bar-item {
|
||||
display: flex;
|
||||
flex: 1 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
min-height: 34px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid rgba(15, 23, 42, 0.14);
|
||||
border-radius: var(--hv2-radius-sm);
|
||||
color: var(--hv2-text);
|
||||
background: transparent;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
gap: 18px;
|
||||
min-width: min(100%, 520px);
|
||||
padding: 6px 0;
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
|
||||
.promotion-bar-item + .promotion-bar-item {
|
||||
border-left: 1px solid color-mix(in srgb, var(--accent) 20%, transparent);
|
||||
}
|
||||
|
||||
.promotion-bar-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
color: var(--ink);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.promotion-bar-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex: 0 0 auto;
|
||||
border-radius: var(--oc-radius-inset);
|
||||
}
|
||||
|
||||
.promotion-bar-icon-fallback {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.promotion-bar-title-copy {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
transition:
|
||||
background-color 0.15s ease,
|
||||
color 0.15s ease,
|
||||
transform 0.15s ease;
|
||||
}
|
||||
|
||||
.home-v2-promotion-link:hover {
|
||||
background: rgba(15, 23, 42, 0.04);
|
||||
color: var(--hv2-text);
|
||||
text-decoration: none;
|
||||
transform: translateY(-1px);
|
||||
.promotion-bar-copy {
|
||||
display: flex;
|
||||
flex: 0 1 auto;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
min-width: 0;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-theme-resolved="light"] .home-v2-promotion-link {
|
||||
border-color: var(--border-ui);
|
||||
color: #171717;
|
||||
background: var(--surface);
|
||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06);
|
||||
.promotion-bar-separator {
|
||||
width: 1px;
|
||||
height: 14px;
|
||||
flex: 0 0 auto;
|
||||
background: color-mix(in srgb, var(--ink) 18%, transparent);
|
||||
}
|
||||
|
||||
[data-theme-resolved="light"] .home-v2-promotion-link:hover {
|
||||
border-color: var(--border-ui-hover);
|
||||
background: var(--surface-muted);
|
||||
color: #171717;
|
||||
.promotion-bar-meta {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--ink-soft);
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
[data-theme-resolved="dark"] .home-v2-promotion-card {
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.12),
|
||||
inset 1px 0 0 rgba(255, 255, 255, 0.04);
|
||||
backdrop-filter: blur(10px) saturate(1.08);
|
||||
}
|
||||
|
||||
[data-theme-resolved="dark"] .home-v2-promotion-card::before {
|
||||
background:
|
||||
radial-gradient(
|
||||
ellipse 56% 72% at 26% -8%,
|
||||
rgba(255, 255, 255, 0.08) 0%,
|
||||
rgba(255, 255, 255, 0.03) 38%,
|
||||
rgba(255, 255, 255, 0) 72%
|
||||
),
|
||||
radial-gradient(
|
||||
ellipse 90% 140% at 76% 136%,
|
||||
rgba(220, 38, 64, 0.36) 0%,
|
||||
rgba(220, 38, 64, 0.2) 42%,
|
||||
rgba(220, 38, 64, 0) 76%
|
||||
),
|
||||
radial-gradient(
|
||||
ellipse 44% 76% at 98% 126%,
|
||||
rgba(255, 77, 46, 0.18) 0%,
|
||||
rgba(255, 77, 46, 0) 70%
|
||||
);
|
||||
mix-blend-mode: normal;
|
||||
}
|
||||
|
||||
[data-theme-resolved="dark"] .home-v2-promotion-card::after {
|
||||
opacity: 0.04;
|
||||
}
|
||||
|
||||
[data-theme-resolved="dark"] .home-v2-promotion-title-brand {
|
||||
color: rgba(255, 255, 255, 0.94);
|
||||
}
|
||||
|
||||
[data-theme-resolved="dark"] .home-v2-promotion-title-muted {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
[data-theme-resolved="dark"] .home-v2-promotion-meta {
|
||||
color: rgba(255, 255, 255, 0.52);
|
||||
}
|
||||
|
||||
[data-theme-resolved="dark"] .home-v2-promotion-days {
|
||||
color: rgba(255, 255, 255, 0.56);
|
||||
}
|
||||
|
||||
[data-theme-resolved="dark"] .home-v2-promotion-link {
|
||||
border: 0;
|
||||
color: #111;
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
}
|
||||
|
||||
[data-theme-resolved="dark"] .home-v2-promotion-link:hover {
|
||||
background: white;
|
||||
color: #000;
|
||||
.promotion-bar-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-height: 24px;
|
||||
padding: 0 8px;
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 28%, transparent);
|
||||
border-radius: var(--oc-radius-control);
|
||||
background: color-mix(in srgb, var(--accent) 10%, transparent);
|
||||
color: var(--accent);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.home-v2-promotions {
|
||||
padding: 0 20px 48px;
|
||||
.promotion-bar-link:hover {
|
||||
border-color: color-mix(in srgb, var(--accent) 42%, transparent);
|
||||
background: color-mix(in srgb, var(--accent) 15%, transparent);
|
||||
color: var(--accent-hover);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.promotion-bar-link svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.promotion-bar-track {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.home-v2-promotion-card {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
gap: 22px;
|
||||
min-height: 0;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.home-v2-promotion-content {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.home-v2-promotion-title,
|
||||
.home-v2-promotion-meta {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.home-v2-promotion-title {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.home-v2-promotion-meta {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.home-v2-promotion-actions {
|
||||
.promotion-bar-item {
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
min-width: 100%;
|
||||
padding: 7px 12px;
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.home-v2-promotion-link {
|
||||
min-height: 34px;
|
||||
padding: 0 14px;
|
||||
font-size: 13px;
|
||||
.promotion-bar-copy {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.promotion-bar-separator,
|
||||
.promotion-bar-meta {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user