Fix case-study share chrome, hero lockup width, and copy confirmation in the share dialog.

Marketing pages omitted create-flow nav copy, so Share rendered as a translation key; the case-study banner squeezed hero text to the thumbnail width; Copy link had no hover or copied state.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
adilallo
2026-09-09 22:00:19 -06:00
co-authored by Cursor
parent ddf7b0ef7a
commit ce581a42c8
17 changed files with 225 additions and 52 deletions
@@ -16,7 +16,7 @@ import type { ContentContainerProps } from "./ContentContainer.types";
const ContentContainerContainer = memo<ContentContainerProps>(
({
post,
width = "200px",
width: widthProp,
size: sizeProp = "responsive",
tone: toneProp = "inverse",
leadingImageSrc,
@@ -26,6 +26,8 @@ const ContentContainerContainer = memo<ContentContainerProps>(
const size = sizeProp;
const tone = toneProp;
const showLeadingImage = showLeadingImageProp;
const width =
widthProp ?? (size === "useCase" ? "100%" : "200px");
const onLight = tone === "onLight";
const titleColor = onLight
? "text-[var(--color-content-default-primary)] group-hover:text-[var(--color-content-default-brand-primary)]"
@@ -57,40 +59,56 @@ const ContentContainerContainer = memo<ContentContainerProps>(
},
);
const isUseCase = size === "useCase";
const containerClasses =
size === "xs"
? "relative z-20 flex h-full flex-col gap-[var(--measures-spacing-012)]"
: "relative z-20 h-full flex flex-col gap-[var(--measures-spacing-012)] sm:gap-[var(--measures-spacing-016)] md:gap-[18px] lg:gap-[var(--measures-spacing-024)]";
: isUseCase
? "relative z-20 flex h-full w-full min-w-0 flex-col gap-[var(--measures-spacing-024)]"
: "relative z-20 h-full flex flex-col gap-[var(--measures-spacing-012)] sm:gap-[var(--measures-spacing-016)] md:gap-[18px] lg:gap-[var(--measures-spacing-024)]";
const contentGapClasses =
size === "xs"
? "flex flex-col gap-[var(--measures-spacing-008)]"
: "flex flex-col gap-[var(--measures-spacing-008)] sm:gap-[var(--measures-spacing-012)] md:gap-[var(--measures-spacing-008)] lg:gap-[var(--measures-spacing-016)] xl:gap-[var(--measures-spacing-004)]";
: isUseCase
? "flex w-full min-w-0 flex-col gap-[var(--measures-spacing-016)]"
: "flex flex-col gap-[var(--measures-spacing-008)] sm:gap-[var(--measures-spacing-012)] md:gap-[var(--measures-spacing-008)] lg:gap-[var(--measures-spacing-016)] xl:gap-[var(--measures-spacing-004)]";
const textGapClasses =
size === "xs"
? "flex flex-col gap-[var(--measures-spacing-004)]"
: "flex flex-col gap-[var(--measures-spacing-004)] md:gap-[var(--measures-spacing-002)] lg:gap-[var(--measures-spacing-004)]";
: isUseCase
? "flex w-full min-w-0 flex-col gap-[var(--measures-spacing-004)]"
: "flex flex-col gap-[var(--measures-spacing-004)] md:gap-[var(--measures-spacing-002)] lg:gap-[var(--measures-spacing-004)]";
const titleClasses =
size === "xs"
? `font-bricolage-grotesque font-medium text-[18px] leading-[22px] transition-colors ${titleColor}`
: `font-bricolage-grotesque font-medium text-xx-small-display sm:text-x-small-display md:text-[32px] md:leading-[110%] lg:text-medium-display xl:text-x-large-display transition-colors ${titleColor}`;
: isUseCase
? `w-full font-bricolage-grotesque font-medium text-[32px] leading-[110%] lg:text-medium-display transition-colors ${titleColor}`
: `font-bricolage-grotesque font-medium text-xx-small-display sm:text-x-small-display md:text-[32px] md:leading-[110%] lg:text-medium-display xl:text-x-large-display transition-colors ${titleColor}`;
const descriptionClasses =
size === "xs"
? `text-x-small-paragraph max-w-md ${bodyColor}`
: `text-x-small-paragraph sm:text-small-paragraph md:text-small-paragraph lg:text-large-paragraph xl:text-x-large-paragraph ${bodyColor}`;
: isUseCase
? `w-full text-small-paragraph lg:text-large-paragraph ${bodyColor}`
: `text-x-small-paragraph sm:text-small-paragraph md:text-small-paragraph lg:text-large-paragraph xl:text-x-large-paragraph ${bodyColor}`;
const authorClasses =
size === "xs"
? `overflow-hidden text-ellipsis whitespace-nowrap text-xx-small-paragraph ${bodyColor}`
: `text-xx-small-paragraph md:text-x-small-paragraph lg:text-small-paragraph xl:text-large-paragraph ${bodyColor}`;
: isUseCase
? `overflow-hidden text-ellipsis whitespace-nowrap text-x-small-paragraph lg:text-small-paragraph ${bodyColor}`
: `text-xx-small-paragraph md:text-x-small-paragraph lg:text-small-paragraph xl:text-large-paragraph ${bodyColor}`;
const dateClasses =
size === "xs"
? `overflow-hidden text-ellipsis whitespace-nowrap text-xx-small-paragraph ${bodyColor}`
: `text-xx-small-paragraph md:text-x-small-paragraph lg:text-small-paragraph xl:text-large-paragraph ${bodyColor}`;
: isUseCase
? `overflow-hidden text-ellipsis whitespace-nowrap text-x-small-paragraph lg:text-small-paragraph ${bodyColor}`
: `text-xx-small-paragraph md:text-x-small-paragraph lg:text-small-paragraph xl:text-large-paragraph ${bodyColor}`;
return (
<ContentContainerView
@@ -1,6 +1,5 @@
import type { BlogPost } from "../../../../lib/content";
export type ContentContainerSizeValue = "xs" | "responsive";
import type { ContentContainerSizeValue } from "../../../../lib/propNormalization";
/** `inverse` — blog hero on imagery; `onLight` — marketing pages on default surface. */
export type ContentContainerToneValue = "inverse" | "onLight";
@@ -9,7 +8,8 @@ export interface ContentContainerProps {
post: BlogPost;
width?: string;
/**
* Content container size.
* `xs` — catalog thumbnail. `responsive` — article banner (scales through xl).
* `useCase` — case-study ContentBanner lockup (Figma 365px / medium-display).
*/
size?: ContentContainerSizeValue;
/**
@@ -27,7 +27,7 @@ export interface ContentContainerProps {
export interface ContentContainerViewProps {
post: BlogPost;
width: string;
size: "xs" | "responsive";
size: ContentContainerSizeValue;
tone: ContentContainerToneValue;
iconImage: string;
iconAlt: string;
@@ -3,8 +3,9 @@
/**
* Figma: Community Rule System — "Modal / Share"
* https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22073-30884
* Copy-link hover / Copied! : 23769:27494 / 23769:27443
*/
import { memo, useId, useRef } from "react";
import { memo, useCallback, useEffect, useId, useRef, useState } from "react";
import { useTranslation } from "../../../contexts/MessagesContext";
import { useCreateModalA11y } from "../Create/useCreateModalA11y";
import { ShareView } from "./Share.view";
@@ -15,9 +16,27 @@ const ShareContainer = memo<ShareProps>((props) => {
const overlayRef = useRef<HTMLDivElement>(null);
const titleId = useId();
const t = useTranslation("modals.share");
const [linkCopied, setLinkCopied] = useState(false);
useCreateModalA11y(props.isOpen, props.onClose, dialogRef);
useEffect(() => {
if (!props.isOpen) {
setLinkCopied(false);
}
}, [props.isOpen]);
const onCopyLinkClick = useCallback(async () => {
try {
const result = await props.onCopyLink();
if (result !== false) {
setLinkCopied(true);
}
} catch {
setLinkCopied(false);
}
}, [props.onCopyLink]);
return (
<ShareView
{...props}
@@ -27,6 +46,10 @@ const ShareContainer = memo<ShareProps>((props) => {
title={t("title")}
description={t("description")}
copyLinkLabel={t("copyLink")}
copiedLabel={t("copied")}
copiedLive={t("copiedLive")}
linkCopied={linkCopied}
onCopyLinkClick={onCopyLinkClick}
signalLabel={t("signal")}
slackLabel={t("slack")}
discordLabel={t("discord")}
+7 -1
View File
@@ -4,7 +4,8 @@ import type { CreateModalBackdropVariant } from "../Create/CreateModalFrame.view
export type ShareProps = {
isOpen: boolean;
onClose: () => void;
onCopyLink: () => void | Promise<void>;
/** Return `false` when the clipboard write did not succeed. */
onCopyLink: () => void | boolean | Promise<void | boolean>;
onEmailShare: () => void;
onSignalShare: () => void | Promise<void>;
onSlackShare: () => void | Promise<void>;
@@ -20,6 +21,10 @@ export type ShareViewProps = ShareProps & {
title: string;
description: string;
copyLinkLabel: string;
copiedLabel: string;
copiedLive: string;
linkCopied: boolean;
onCopyLinkClick: () => void | Promise<void>;
signalLabel: string;
slackLabel: string;
discordLabel: string;
@@ -34,4 +39,5 @@ export type ShareChannelTileProps = {
onClick: () => void | Promise<void>;
circleClassName: string;
icon: ReactNode;
copied?: boolean;
};
+43 -13
View File
@@ -34,19 +34,32 @@ function ShareAssetIcon(props: {
);
}
function ShareChannelTile({ label, onClick, circleClassName, icon }: ShareChannelTileProps) {
function ShareChannelTile({
label,
onClick,
circleClassName,
icon,
copied = false,
}: ShareChannelTileProps) {
return (
<button
type="button"
onClick={() => void onClick()}
className="flex w-16 shrink-0 flex-col items-center gap-2 rounded-md focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-invert-primary)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-surface-default-primary)]"
aria-pressed={copied || undefined}
className="group flex w-16 shrink-0 flex-col items-center gap-2 rounded-md focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-invert-primary)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-surface-default-primary)]"
>
<div
className={`flex h-[60px] w-[60px] items-center justify-center rounded-full border border-solid ${circleClassName}`}
className={`flex h-[60px] w-[60px] items-center justify-center rounded-full border border-solid transition-[transform,background-color,border-color,filter] duration-150 ease-out group-active:scale-90 ${circleClassName}`}
>
{icon}
</div>
<span className="max-w-[4.5rem] text-center text-x-small-label text-[var(--color-content-default-tertiary)]">
<span
className={`max-w-[4.5rem] text-center text-x-small-label ${
copied
? "text-[var(--color-border-default-positive-primary)]"
: "text-[var(--color-content-default-tertiary)]"
}`}
>
{label}
</span>
</button>
@@ -56,7 +69,6 @@ function ShareChannelTile({ label, onClick, circleClassName, icon }: ShareChanne
export const ShareView = memo(function ShareView({
isOpen,
onClose,
onCopyLink,
onEmailShare,
onSignalShare,
onSlackShare,
@@ -69,6 +81,10 @@ export const ShareView = memo(function ShareView({
title,
description,
copyLinkLabel,
copiedLabel,
copiedLive,
linkCopied,
onCopyLinkClick,
signalLabel,
slackLabel,
discordLabel,
@@ -109,36 +125,50 @@ export const ShareView = memo(function ShareView({
{/* Channel circle hexes are third-party brand colors (copy/link, Signal, Slack, Discord), not DS tokens. */}
<div className="flex flex-wrap gap-4">
<ShareChannelTile
label={copyLinkLabel}
onClick={onCopyLink}
circleClassName="border-[#444444] bg-[#333333]"
icon={<ShareAssetIcon name="link" width={24} height={24} />}
label={linkCopied ? copiedLabel : copyLinkLabel}
onClick={onCopyLinkClick}
copied={linkCopied}
circleClassName={
linkCopied
? "border-2 border-[#444444] bg-[#333333]"
: "border-[#444444] bg-[#333333] group-hover:border-2 group-hover:border-[var(--color-border-default-positive-primary)] group-hover:bg-[#444444]"
}
icon={
<ShareAssetIcon
name={linkCopied ? "check" : "link"}
width={24}
height={24}
/>
}
/>
<ShareChannelTile
label={signalLabel}
onClick={onSignalShare}
circleClassName="border-[#3a76f0] bg-[#3a76f0]"
circleClassName="border-[#3a76f0] bg-[#3a76f0] group-hover:brightness-110"
icon={<ShareAssetIcon name="signal" width={26} height={26} />}
/>
<ShareChannelTile
label={slackLabel}
onClick={onSlackShare}
circleClassName="border-[#4a154b] bg-[#4a154b]"
circleClassName="border-[#4a154b] bg-[#4a154b] group-hover:brightness-110"
icon={<ShareAssetIcon name="slack" width={26} height={26} />}
/>
<ShareChannelTile
label={discordLabel}
onClick={onDiscordShare}
circleClassName="border-[#5865f2] bg-[#5865f2]"
circleClassName="border-[#5865f2] bg-[#5865f2] group-hover:brightness-110"
icon={<ShareAssetIcon name="discord" width={30} height={30} />}
/>
<ShareChannelTile
label={emailLabel}
onClick={onEmailShare}
circleClassName="border-[var(--color-surface-default-brand-kiwi)] bg-[var(--color-surface-default-brand-kiwi)]"
circleClassName="border-[var(--color-surface-default-brand-kiwi)] bg-[var(--color-surface-default-brand-kiwi)] group-hover:brightness-110"
icon={<ShareAssetIcon name="mail" width={24} height={24} />}
/>
</div>
<p className="sr-only" aria-live="polite">
{linkCopied ? copiedLive : ""}
</p>
</div>
<ModalFooter
@@ -193,7 +193,7 @@ function ContentBannerUseCaseView({
>
<ContentContainer
post={post}
size="responsive"
size="useCase"
tone={contentTone}
showLeadingImage={false}
leadingImageSrc={leadingImageSrc}