Implement share and export components

This commit is contained in:
adilallo
2026-04-29 22:27:46 -06:00
parent a31a36c926
commit a37a72c71d
58 changed files with 3153 additions and 117 deletions
@@ -3,5 +3,9 @@ export interface ModalHeaderProps {
onMoreOptions?: () => void;
showCloseButton?: boolean;
showMoreOptionsButton?: boolean;
/** When set, used for the close controls accessible name (e.g. localized). */
closeButtonAriaLabel?: string;
/** When set, used for the more-options controls accessible name (e.g. localized). */
moreOptionsAriaLabel?: string;
className?: string;
}
@@ -9,6 +9,8 @@ export function ModalHeaderView({
onMoreOptions,
showCloseButton = true,
showMoreOptionsButton = true,
closeButtonAriaLabel = "Close dialog",
moreOptionsAriaLabel = "More options",
className = "",
}: ModalHeaderProps) {
return (
@@ -21,7 +23,7 @@ export function ModalHeaderView({
type="button"
onClick={onClose}
className={`${iconButtonClass} left-[24px] top-[12px]`}
aria-label="Close dialog"
aria-label={closeButtonAriaLabel}
>
{/* eslint-disable-next-line @next/next/no-img-element -- icon asset */}
<img
@@ -41,7 +43,7 @@ export function ModalHeaderView({
type="button"
onClick={onMoreOptions}
className={`${iconButtonClass} right-[24px] top-[12px]`}
aria-label="More options"
aria-label={moreOptionsAriaLabel}
>
<svg
width="16"
@@ -0,0 +1,17 @@
"use client";
/**
* Figma: Community Rule System — Export popover (Community Rule System · 21998:22612)
* https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=21998-22612
*/
import { memo } from "react";
import { PopoverView } from "./Popover.view";
import type { PopoverProps } from "./Popover.types";
const Popover = memo<PopoverProps>((props) => {
return <PopoverView {...props} />;
});
Popover.displayName = "Popover";
export default Popover;
@@ -0,0 +1,8 @@
import type { ReactNode } from "react";
export type PopoverProps = {
id: string;
menuAriaLabel: string;
children: ReactNode;
className?: string;
};
@@ -0,0 +1,25 @@
"use client";
import { memo } from "react";
import type { PopoverProps } from "./Popover.types";
export const PopoverView = memo(function PopoverView({
id,
menuAriaLabel,
children,
className = "",
}: PopoverProps) {
return (
<div
id={id}
role="menu"
aria-label={menuAriaLabel}
data-figma-node="21998:22612"
className={`flex min-w-[171px] w-max max-w-[calc(100vw-32px)] flex-col items-stretch overflow-hidden rounded-[var(--radius-300)] bg-[var(--color-surface-default-secondary)] px-[12px] [filter:drop-shadow(0px_0px_6px_rgba(254,252,201,0.2))] ${className}`}
>
{children}
</div>
);
});
PopoverView.displayName = "PopoverView";
+2
View File
@@ -0,0 +1,2 @@
export { default } from "./Popover.container";
export type { PopoverProps } from "./Popover.types";
@@ -0,0 +1,43 @@
"use client";
/**
* Figma: Community Rule System — "Modal / Share"
* https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22073-30884
*/
import { memo, useId, useRef } from "react";
import { useTranslation } from "../../../contexts/MessagesContext";
import { useCreateModalA11y } from "../Create/useCreateModalA11y";
import { ShareView } from "./Share.view";
import type { ShareProps } from "./Share.types";
const ShareContainer = memo<ShareProps>((props) => {
const dialogRef = useRef<HTMLDivElement>(null);
const overlayRef = useRef<HTMLDivElement>(null);
const titleId = useId();
const t = useTranslation("modals.share");
useCreateModalA11y(props.isOpen, props.onClose, dialogRef);
return (
<ShareView
{...props}
dialogRef={dialogRef}
overlayRef={overlayRef}
titleId={titleId}
title={t("title")}
description={t("description")}
copyLinkLabel={t("copyLink")}
signalLabel={t("signal")}
slackLabel={t("slack")}
discordLabel={t("discord")}
emailLabel={t("email")}
doneLabel={t("done")}
closeDialogAriaLabel={t("closeDialogAriaLabel")}
moreOptionsAriaLabel={t("moreOptionsAriaLabel")}
/>
);
});
ShareContainer.displayName = "Share";
export default ShareContainer;
@@ -0,0 +1,37 @@
import type { ReactNode, RefObject } from "react";
import type { CreateModalBackdropVariant } from "../Create/CreateModalFrame.view";
export type ShareProps = {
isOpen: boolean;
onClose: () => void;
onCopyLink: () => void | Promise<void>;
onEmailShare: () => void;
onSignalShare: () => void | Promise<void>;
onSlackShare: () => void | Promise<void>;
onDiscordShare: () => void | Promise<void>;
className?: string;
backdropVariant?: CreateModalBackdropVariant;
};
export type ShareViewProps = ShareProps & {
dialogRef: RefObject<HTMLDivElement | null>;
overlayRef: RefObject<HTMLDivElement | null>;
titleId: string;
title: string;
description: string;
copyLinkLabel: string;
signalLabel: string;
slackLabel: string;
discordLabel: string;
emailLabel: string;
doneLabel: string;
closeDialogAriaLabel: string;
moreOptionsAriaLabel: string;
};
export type ShareChannelTileProps = {
label: string;
onClick: () => void | Promise<void>;
circleClassName: string;
icon: ReactNode;
};
+165
View File
@@ -0,0 +1,165 @@
"use client";
import Image from "next/image";
import { memo } from "react";
import ContentLockup from "../../type/ContentLockup";
import Button from "../../buttons/Button";
import ModalHeader from "../ModalHeader";
import ModalFooter from "../ModalFooter";
import { CreateModalFrameView } from "../Create/CreateModalFrame.view";
import type { ShareChannelTileProps, ShareViewProps } from "./Share.types";
/** Decorative glyphs in `public/assets/Share/` — sizes match prior inline SVGs within the 60×60 circles. */
function ShareAssetIcon(props: {
src:
| "/assets/Share/Discord.svg"
| "/assets/Share/Link.svg"
| "/assets/Share/Mail.svg"
| "/assets/Share/Signal.svg"
| "/assets/Share/Slack.svg";
width: number;
height: number;
}) {
const { src, width, height } = props;
return (
<Image
src={src}
alt=""
width={width}
height={height}
className="shrink-0"
unoptimized
aria-hidden
/>
);
}
function ShareChannelTile({ label, onClick, circleClassName, icon }: 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)]"
>
<div
className={`flex h-[60px] w-[60px] items-center justify-center rounded-full border border-solid ${circleClassName}`}
>
{icon}
</div>
<span className="max-w-[4.5rem] text-center font-inter text-[12px] font-medium leading-4 text-[var(--color-content-default-tertiary)]">
{label}
</span>
</button>
);
}
export const ShareView = memo(function ShareView({
isOpen,
onClose,
onCopyLink,
onEmailShare,
onSignalShare,
onSlackShare,
onDiscordShare,
className = "",
backdropVariant = "default",
dialogRef,
overlayRef,
titleId,
title,
description,
copyLinkLabel,
signalLabel,
slackLabel,
discordLabel,
emailLabel,
doneLabel,
closeDialogAriaLabel,
moreOptionsAriaLabel,
}: ShareViewProps) {
return (
<CreateModalFrameView
isOpen={isOpen}
onOverlayClick={onClose}
backdropVariant={backdropVariant}
className={`max-h-[90vh] w-[min(546px,calc(100vw-32px))] max-w-[546px] min-h-0 ${className}`}
ariaLabel={title}
ariaLabelledBy={titleId}
overlayRef={overlayRef}
dialogRef={dialogRef}
>
<ModalHeader
onClose={onClose}
onMoreOptions={onClose}
closeButtonAriaLabel={closeDialogAriaLabel}
moreOptionsAriaLabel={moreOptionsAriaLabel}
/>
<div className="shrink-0 bg-[var(--color-surface-default-primary)] px-[24px] py-[12px]">
<ContentLockup
title={title}
description={description}
variant="modal"
alignment="left"
titleId={titleId}
/>
</div>
<div className="scrollbar-design flex min-h-0 flex-1 flex-col overflow-x-clip overflow-y-auto px-[24px] pb-6 pt-0">
<div className="flex flex-wrap gap-4">
<ShareChannelTile
label={copyLinkLabel}
onClick={onCopyLink}
circleClassName="border-[#444444] bg-[#333333]"
icon={<ShareAssetIcon src="/assets/Share/Link.svg" width={24} height={24} />}
/>
<ShareChannelTile
label={signalLabel}
onClick={onSignalShare}
circleClassName="border-[#3a76f0] bg-[#3a76f0]"
icon={<ShareAssetIcon src="/assets/Share/Signal.svg" width={26} height={26} />}
/>
<ShareChannelTile
label={slackLabel}
onClick={onSlackShare}
circleClassName="border-[#4a154b] bg-[#4a154b]"
icon={<ShareAssetIcon src="/assets/Share/Slack.svg" width={26} height={26} />}
/>
<ShareChannelTile
label={discordLabel}
onClick={onDiscordShare}
circleClassName="border-[#5865f2] bg-[#5865f2]"
icon={<ShareAssetIcon src="/assets/Share/Discord.svg" width={30} height={30} />}
/>
<ShareChannelTile
label={emailLabel}
onClick={onEmailShare}
circleClassName="border-[var(--color-surface-default-brand-kiwi)] bg-[var(--color-surface-default-brand-kiwi)]"
icon={<ShareAssetIcon src="/assets/Share/Mail.svg" width={24} height={24} />}
/>
</div>
</div>
<ModalFooter
showBackButton={false}
showNextButton={false}
stepper={false}
footerContent={
<div className="absolute right-[16px] top-[12px] flex max-w-[calc(100%-32px)] flex-wrap items-center justify-end gap-3">
<Button
buttonType="filled"
palette="default"
size="medium"
type="button"
onClick={onClose}
>
{doneLabel}
</Button>
</div>
}
/>
</CreateModalFrameView>
);
});
ShareView.displayName = "ShareView";
+2
View File
@@ -0,0 +1,2 @@
export { default } from "./Share.container";
export type { ShareProps } from "./Share.types";