Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95e1937419 | ||
|
|
ce581a42c8 |
@@ -96,7 +96,7 @@ export function useCompletedRuleShareExport({
|
||||
}: {
|
||||
setActionBanner: (_: CompletedFlowActionBanner | null) => void;
|
||||
}): {
|
||||
copyPublishedRuleLink: () => Promise<void>;
|
||||
copyPublishedRuleLink: () => Promise<boolean>;
|
||||
mailtoPublishedRule: () => void;
|
||||
sharePublishedRuleViaSignal: () => Promise<void>;
|
||||
sharePublishedRuleViaSlack: () => Promise<void>;
|
||||
@@ -137,32 +137,34 @@ export function useCompletedRuleShareExport({
|
||||
url: string,
|
||||
banner?: () => void,
|
||||
options?: { suppressFailureWhenDocumentNotFocused?: boolean },
|
||||
) => {
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(url);
|
||||
(banner ?? bannerCopied)();
|
||||
return true;
|
||||
} catch {
|
||||
if (
|
||||
options?.suppressFailureWhenDocumentNotFocused === true &&
|
||||
typeof window !== "undefined" &&
|
||||
shouldSkipShareClipboardFallback(window)
|
||||
) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
bannerCopyFailed();
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[bannerCopied, bannerCopyFailed],
|
||||
);
|
||||
|
||||
const copyPublishedRuleLink = useCallback(async () => {
|
||||
if (typeof window === "undefined") return;
|
||||
if (typeof window === "undefined") return false;
|
||||
const ctx = resolvePublishedRuleShareContext(window);
|
||||
if (!ctx) {
|
||||
bannerNoRule();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
await copyUrlToClipboard(ctx.url);
|
||||
return copyUrlToClipboard(ctx.url);
|
||||
}, [bannerNoRule, copyUrlToClipboard]);
|
||||
|
||||
const mailtoPublishedRule = useCallback(() => {
|
||||
|
||||
+3
-1
@@ -34,7 +34,7 @@ export function useUseCaseCompletedRuleActions({
|
||||
const [duplicateBusy, setDuplicateBusy] = useState(false);
|
||||
|
||||
const copyPageLink = useCallback(async () => {
|
||||
if (typeof window === "undefined") return;
|
||||
if (typeof window === "undefined") return false;
|
||||
try {
|
||||
await navigator.clipboard.writeText(window.location.href);
|
||||
setActionBanner({
|
||||
@@ -43,6 +43,7 @@ export function useUseCaseCompletedRuleActions({
|
||||
title: t("shareLinkCopiedTitle"),
|
||||
description: t("shareLinkCopiedDescription"),
|
||||
});
|
||||
return true;
|
||||
} catch {
|
||||
setActionBanner({
|
||||
key: "shareCopyFailed",
|
||||
@@ -50,6 +51,7 @@ export function useUseCaseCompletedRuleActions({
|
||||
title: t("shareCopyFailedTitle"),
|
||||
description: t("shareCopyFailedDescription"),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}, [setActionBanner, t]);
|
||||
|
||||
|
||||
@@ -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,39 +59,55 @@ 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)]"
|
||||
: 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)]"
|
||||
: 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)]"
|
||||
: 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}`
|
||||
: 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}`
|
||||
: 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}`
|
||||
: 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}`
|
||||
: 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 (
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
+7
-1
@@ -86,7 +86,13 @@ export function partnerLogoPath(slug: string): string {
|
||||
}
|
||||
|
||||
/** Share modal glyphs in `public/assets/share/`. */
|
||||
export type ShareIconName = "discord" | "link" | "mail" | "signal" | "slack";
|
||||
export type ShareIconName =
|
||||
| "check"
|
||||
| "discord"
|
||||
| "link"
|
||||
| "mail"
|
||||
| "signal"
|
||||
| "slack";
|
||||
|
||||
export function shareIconPath(name: ShareIconName): string {
|
||||
return `assets/share/${name}.svg`;
|
||||
|
||||
@@ -119,7 +119,11 @@ export type HeaderLockupPaletteValue =
|
||||
export const TEXT_INPUT_SIZE_OPTIONS = ["small", "medium"] as const;
|
||||
export type TextInputSizeValue = (typeof TEXT_INPUT_SIZE_OPTIONS)[number];
|
||||
|
||||
export const CONTENT_CONTAINER_SIZE_OPTIONS = ["xs", "responsive"] as const;
|
||||
export const CONTENT_CONTAINER_SIZE_OPTIONS = [
|
||||
"xs",
|
||||
"responsive",
|
||||
"useCase",
|
||||
] as const;
|
||||
export type ContentContainerSizeValue =
|
||||
(typeof CONTENT_CONTAINER_SIZE_OPTIONS)[number];
|
||||
|
||||
|
||||
+14
-13
@@ -1,17 +1,15 @@
|
||||
/**
|
||||
* Marketing-scoped message bundle: every namespace from `./index` EXCEPT the
|
||||
* `create.*` subtree. The `create` namespace is ~41 KB gzipped — the largest
|
||||
* single contributor to per-route HTML size — and is only used inside
|
||||
* `(app)/create/*`. Excluding it from the `(marketing)` group's
|
||||
* `MessagesProvider` removes the embed from every marketing HTML response.
|
||||
* bulk of the `create.*` subtree. That subtree is ~41 KB gzipped — the
|
||||
* largest single contributor to per-route HTML size — and wizard copy is
|
||||
* only used inside `(app)/create/*`.
|
||||
*
|
||||
* The type stays compatible with `typeof import("./index").default` because
|
||||
* we satisfy the same key shape (modulo `create`); marketing client
|
||||
* components only read keys that exist here. If a future change reaches into
|
||||
* `messages.create.*` from a marketing surface, `getTranslation` will return
|
||||
* the dotted key as the fallback — visible immediately at runtime.
|
||||
* Case-study completed-rule chrome reuses `CreateFlowTopNav`, so this bundle
|
||||
* includes `create.topNav` only. Other `create.*` keys still fall back to the
|
||||
* dotted path via `getTranslation` if a marketing surface reaches them.
|
||||
*
|
||||
* Keep this in sync with new entries added to `./index` (excluding `create/`).
|
||||
* Keep this in sync with new entries added to `./index` (excluding wizard
|
||||
* `create/` files other than `create/topNav.json`).
|
||||
* See `docs/perf/next16-eval.md` for measurement context.
|
||||
*/
|
||||
import common from "./common.json";
|
||||
@@ -53,6 +51,7 @@ import metadata from "./metadata.json";
|
||||
import modalsShare from "./modals/share.json";
|
||||
import modalsPopoverExport from "./modals/popoverExport.json";
|
||||
import modalsAskOrganizerInquiry from "./modals/askOrganizerInquiry.json";
|
||||
import createTopNav from "./create/topNav.json";
|
||||
import type messages from "./index";
|
||||
|
||||
const marketingMessages = {
|
||||
@@ -99,11 +98,13 @@ const marketingMessages = {
|
||||
popoverExport: modalsPopoverExport,
|
||||
askOrganizerInquiry: modalsAskOrganizerInquiry,
|
||||
},
|
||||
create: {
|
||||
topNav: createTopNav,
|
||||
},
|
||||
};
|
||||
|
||||
// Cast to the full shape so it satisfies `typeof import("./index").default`
|
||||
// at the MessagesProvider boundary. Reads of `messages.create.*` from a
|
||||
// marketing surface are a code smell and will return the dotted key (the
|
||||
// runtime `getTranslation` fallback) — visible immediately.
|
||||
// at the MessagesProvider boundary. Reads of wizard `create.*` keys other
|
||||
// than `create.topNav` still return the dotted key at runtime.
|
||||
export default marketingMessages as typeof messages;
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
"title": "Share this CommunityRule",
|
||||
"description": "Anyone with the link can view this rule.",
|
||||
"copyLink": "Copy link",
|
||||
"copied": "Copied!",
|
||||
"copiedLive": "Link copied to clipboard",
|
||||
"signal": "Signal",
|
||||
"slack": "Slack",
|
||||
"discord": "Discord",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 6L9 17L4 12" stroke="#45EA97" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 188 B |
@@ -157,11 +157,14 @@ describe("ContentBanner", () => {
|
||||
|
||||
const title = screen.getByRole("heading", { name: "Test Article" });
|
||||
expect(title).toBeInTheDocument();
|
||||
expect(title).toHaveClass("sm:text-x-small-display", "md:text-[32px]");
|
||||
expect(title).toHaveClass("text-[32px]", "lg:text-medium-display");
|
||||
expect(title).not.toHaveClass("xl:text-x-large-display");
|
||||
expect(screen.getByText("Sample Operating Manual")).toBeInTheDocument();
|
||||
const copyColumn = container.querySelector('[data-node-id="19189:9171"]');
|
||||
expect(copyColumn).toHaveClass("lg:max-w-[365px]");
|
||||
expect(copyColumn).not.toHaveClass("max-w-[365px]");
|
||||
const copyLockup = title.closest("div.relative.z-20");
|
||||
expect(copyLockup).toHaveStyle({ width: "100%" });
|
||||
const bannerRow = container.querySelector(
|
||||
'[data-figma-node="22015:42621"]',
|
||||
);
|
||||
|
||||
@@ -70,6 +70,43 @@ describe("Share modal", () => {
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("confirms Copy link with Copied! when the handler succeeds", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onCopyLink = vi.fn().mockResolvedValue(true);
|
||||
render(
|
||||
<Share
|
||||
isOpen={true}
|
||||
onClose={vi.fn()}
|
||||
{...noopHandlers}
|
||||
onCopyLink={onCopyLink}
|
||||
/>,
|
||||
);
|
||||
await user.click(screen.getByRole("button", { name: "Copy link" }));
|
||||
expect(onCopyLink).toHaveBeenCalledTimes(1);
|
||||
expect(screen.getByRole("button", { name: "Copied!" })).toHaveAttribute(
|
||||
"aria-pressed",
|
||||
"true",
|
||||
);
|
||||
expect(screen.getByText("Link copied to clipboard")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps Copy link when the handler reports failure", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<Share
|
||||
isOpen={true}
|
||||
onClose={vi.fn()}
|
||||
{...noopHandlers}
|
||||
onCopyLink={vi.fn().mockResolvedValue(false)}
|
||||
/>,
|
||||
);
|
||||
await user.click(screen.getByRole("button", { name: "Copy link" }));
|
||||
expect(screen.getByRole("button", { name: "Copy link" })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "Copied!" }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls onClose when header overflow (more) is activated, matching modal chrome parity", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onClose = vi.fn();
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { describe, test, expect, vi } from "vitest";
|
||||
import { screen } from "@testing-library/react";
|
||||
import { screen, render as rtlRender } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithProviders as render } from "../utils/test-utils";
|
||||
import { MessagesProvider } from "../../app/contexts/MessagesContext";
|
||||
import UseCaseCompletedRulePage from "../../app/(marketing-case-study)/use-cases/[slug]/rule/page";
|
||||
import messages from "../../messages/en/index";
|
||||
import marketingMessages from "../../messages/en/marketing";
|
||||
import { getTranslation } from "../../lib/i18n/getTranslation";
|
||||
import { USE_CASE_DETAIL_SLUGS } from "../../lib/useCaseSyntheticPost";
|
||||
|
||||
const mockPush = vi.fn();
|
||||
@@ -83,9 +86,31 @@ describe("UseCaseCompletedRulePage", () => {
|
||||
name: messages.pages.useCasesCompletedRule.topNav.duplicateAriaLabel,
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Share" })).toBeInTheDocument();
|
||||
},
|
||||
);
|
||||
|
||||
test("marketing message bundle resolves Share (not the dotted create.topNav key)", () => {
|
||||
expect(getTranslation(marketingMessages, "create.topNav.share")).toBe(
|
||||
"Share",
|
||||
);
|
||||
});
|
||||
|
||||
test("Share button reads Share under the marketing case-study bundle", async () => {
|
||||
rtlRender(
|
||||
<MessagesProvider messages={marketingMessages}>
|
||||
{await UseCaseCompletedRulePage({
|
||||
params: Promise.resolve({ slug: "mutual-aid-colorado" }),
|
||||
})}
|
||||
</MessagesProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("button", { name: "Share" })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "create.topNav.share" }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("Duplicate opens login when signed out", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockOpenLogin.mockClear();
|
||||
|
||||
@@ -111,6 +111,17 @@ describe("ContentContainer", () => {
|
||||
expect(container).toHaveStyle("width: 200px");
|
||||
});
|
||||
|
||||
it("applies full width and case-study type scale for useCase size", () => {
|
||||
render(<ContentContainer post={mockPost} size="useCase" />);
|
||||
|
||||
const container = document.querySelector("div[class*='relative z-20']");
|
||||
expect(container).toHaveStyle("width: 100%");
|
||||
|
||||
const title = screen.getByText("Test Article Title");
|
||||
expect(title).toHaveClass("text-[32px]", "lg:text-medium-display");
|
||||
expect(title).not.toHaveClass("xl:text-x-large-display");
|
||||
});
|
||||
|
||||
it("has proper spacing between icon and text", () => {
|
||||
render(<ContentContainer post={mockPost} />);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user