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
+9
View File
@@ -7,18 +7,24 @@ import ContentCopyIcon from "./content_copy.svg";
import EditIcon from "./edit.svg";
import ExclamationIcon from "./exclamation.svg";
import ChevronRightIcon from "./chevron_right.svg";
import CsvIcon from "./csv.svg";
import LogOutIcon from "./log_out.svg";
import MailIcon from "./mail.svg";
import MarkdownCopyIcon from "./markdown_copy.svg";
import PictureAsPdfIcon from "./picture_as_pdf.svg";
import WarningIcon from "./warning.svg";
export const ICON_NAME_OPTIONS = [
"arrow_back",
"chevron_right",
"content_copy",
"csv",
"edit",
"exclamation",
"log_out",
"mail",
"markdown_copy",
"picture_as_pdf",
"warning",
] as const;
@@ -33,10 +39,13 @@ const iconMap: Record<IconName, SvgComponent> = {
arrow_back: ArrowBackIcon,
chevron_right: ChevronRightIcon,
content_copy: ContentCopyIcon,
csv: CsvIcon,
edit: EditIcon,
exclamation: ExclamationIcon,
log_out: LogOutIcon,
mail: MailIcon,
markdown_copy: MarkdownCopyIcon,
picture_as_pdf: PictureAsPdfIcon,
warning: WarningIcon,
};
+13
View File
@@ -0,0 +1,13 @@
<svg
width="24"
height="24"
viewBox="0 0 20 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid meet"
>
<path
d="M3.75 11H6.75V9.5H4.25V6.5H6.75V5H3.75C3.46667 5 3.22917 5.09583 3.0375 5.2875C2.84583 5.47917 2.75 5.71667 2.75 6V10C2.75 10.2833 2.84583 10.5208 3.0375 10.7125C3.22917 10.9042 3.46667 11 3.75 11ZM7.65 11H10.65C10.9333 11 11.1708 10.9042 11.3625 10.7125C11.5542 10.5208 11.65 10.2833 11.65 10V8.5C11.65 8.21667 11.5542 7.95417 11.3625 7.7125C11.1708 7.47083 10.9333 7.35 10.65 7.35H9.15V6.5H11.65V5H8.65C8.36667 5 8.12917 5.09583 7.9375 5.2875C7.74583 5.47917 7.65 5.71667 7.65 6V7.5C7.65 7.78333 7.74583 8.0375 7.9375 8.2625C8.12917 8.4875 8.36667 8.6 8.65 8.6H10.15V9.5H7.65V11ZM14.25 11H15.75L17.5 5H16L15 8.45L14 5H12.5L14.25 11ZM2 16C1.45 16 0.979167 15.8042 0.5875 15.4125C0.195833 15.0208 0 14.55 0 14V2C0 1.45 0.195833 0.979167 0.5875 0.5875C0.979167 0.195833 1.45 0 2 0H18C18.55 0 19.0208 0.195833 19.4125 0.5875C19.8042 0.979167 20 1.45 20 2V14C20 14.55 19.8042 15.0208 19.4125 15.4125C19.0208 15.8042 18.55 16 18 16H2ZM2 14H18V2H2V14Z"
fill="currentColor"
/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,13 @@
<svg
width="24"
height="24"
viewBox="0 0 17 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid meet"
>
<path
d="M6 16C5.45 16 4.97917 15.8042 4.5875 15.4125C4.19583 15.0208 4 14.55 4 14V2C4 1.45 4.19583 0.979167 4.5875 0.5875C4.97917 0.195833 5.45 0 6 0H15C15.55 0 16.0208 0.195833 16.4125 0.5875C16.8042 0.979167 17 1.45 17 2V14C17 14.55 16.8042 15.0208 16.4125 15.4125C16.0208 15.8042 15.55 16 15 16H6ZM6 14H15V2H6V14ZM2 20C1.45 20 0.979167 19.8042 0.5875 19.4125C0.195833 19.0208 0 18.55 0 18V4H2V18H13V20H2ZM7.25 11H8.75V6.5H9.75V9.5H11.25V6.5H12.25V11H13.75V6C13.75 5.71667 13.6542 5.47917 13.4625 5.2875C13.2708 5.09583 13.0333 5 12.75 5H8.25C7.96667 5 7.72917 5.09583 7.5375 5.2875C7.34583 5.47917 7.25 5.71667 7.25 6V11Z"
fill="currentColor"
/>
</svg>

After

Width:  |  Height:  |  Size: 814 B

@@ -0,0 +1,19 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask
id="picture_as_pdf_icon_mask"
style="mask-type:alpha"
maskUnits="userSpaceOnUse"
x="0"
y="0"
width="24"
height="24"
>
<rect width="24" height="24" fill="#D9D9D9" />
</mask>
<g mask="url(#picture_as_pdf_icon_mask)">
<path
d="M9 12.5H10V10.5H11C11.2833 10.5 11.5208 10.4042 11.7125 10.2125C11.9042 10.0208 12 9.78333 12 9.5V8.5C12 8.21667 11.9042 7.97917 11.7125 7.7875C11.5208 7.59583 11.2833 7.5 11 7.5H9V12.5ZM10 9.5V8.5H11V9.5H10ZM13 12.5H15C15.2833 12.5 15.5208 12.4042 15.7125 12.2125C15.9042 12.0208 16 11.7833 16 11.5V8.5C16 8.21667 15.9042 7.97917 15.7125 7.7875C15.5208 7.59583 15.2833 7.5 15 7.5H13V12.5ZM14 11.5V8.5H15V11.5H14ZM17 12.5H18V10.5H19V9.5H18V8.5H19V7.5H17V12.5ZM8 18C7.45 18 6.97917 17.8042 6.5875 17.4125C6.19583 17.0208 6 16.55 6 16V4C6 3.45 6.19583 2.97917 6.5875 2.5875C6.97917 2.19583 7.45 2 8 2H20C20.55 2 21.0208 2.19583 21.4125 2.5875C21.8042 2.97917 22 3.45 22 4V16C22 16.55 21.8042 17.0208 21.4125 17.4125C21.0208 17.8042 20.55 18 20 18H8ZM8 16H20V4H8V16ZM4 22C3.45 22 2.97917 21.8042 2.5875 21.4125C2.19583 21.0208 2 20.55 2 20V6H4V20H18V22H4Z"
fill="currentColor"
/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+8 -8
View File
@@ -115,21 +115,21 @@ const Button = memo<ButtonProps>(
const variantStyles: Record<string, string> = {
filled:
"bg-[var(--color-surface-inverse-primary)] text-[var(--color-content-inverse-primary)] border-[1.5px] border-transparent hover:bg-[var(--color-surface-inverse-primary)] hover:text-[var(--color-content-invert-brand-primary)] hover:border-[var(--color-border-invert-brand-primary)] hover:scale-[1.02] focus:bg-[var(--color-surface-inverse-primary)] focus:text-[var(--color-content-invert-brand-primary)] focus:outline-none focus:border-transparent focus:shadow-[0_0_0px_2px_var(--color-border-default-primary),0_0_0px_4px_var(--color-border-invert-primary)] focus:scale-[1.02] active:bg-[var(--color-surface-invert-brand-primary)] active:text-[var(--color-content-invert-primary)] active:border-[var(--color-border-invert-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-invert-tertiary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100",
"bg-[var(--color-surface-inverse-primary)] text-[var(--color-content-inverse-primary)] border-[1.5px] border-transparent hover:bg-[var(--color-surface-inverse-primary)] hover:text-[var(--color-content-invert-brand-primary)] hover:border-[var(--color-border-invert-brand-primary)] hover:scale-[1.02] focus-visible:bg-[var(--color-surface-inverse-primary)] focus-visible:text-[var(--color-content-invert-brand-primary)] focus-visible:outline-none focus-visible:border-transparent focus-visible:shadow-[0_0_0px_2px_var(--color-border-default-primary),0_0_0px_4px_var(--color-border-invert-primary)] focus-visible:scale-[1.02] active:bg-[var(--color-surface-invert-brand-primary)] active:text-[var(--color-content-invert-primary)] active:border-[var(--color-border-invert-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-invert-tertiary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100",
"filled-inverse":
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border-[1.5px] border-transparent hover:bg-[var(--color-surface-default-primary)] hover:text-[var(--color-content-default-brand-primary)] hover:border-[var(--color-border-default-brand-primary)] hover:scale-[1.02] focus:bg-[var(--color-surface-default-primary)] focus:text-[var(--color-content-default-brand-primary)] focus:outline-none focus:border-transparent focus:shadow-[0_0_0px_2px_var(--color-border-invert-primary),0_0_0px_4px_var(--color-border-default-primary)] focus:scale-[1.02] active:bg-[var(--color-surface-default-brand-primary)] active:text-[var(--color-content-default-primary)] active:border-[var(--color-border-default-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-invert-secondary)] disabled:text-[var(--color-content-default-tertiary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100",
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border-[1.5px] border-transparent hover:bg-[var(--color-surface-default-primary)] hover:text-[var(--color-content-default-brand-primary)] hover:border-[var(--color-border-default-brand-primary)] hover:scale-[1.02] focus-visible:bg-[var(--color-surface-default-primary)] focus-visible:text-[var(--color-content-default-brand-primary)] focus-visible:outline-none focus-visible:border-transparent focus-visible:shadow-[0_0_0px_2px_var(--color-border-invert-primary),0_0_0px_4px_var(--color-border-default-primary)] focus-visible:scale-[1.02] active:bg-[var(--color-surface-default-brand-primary)] active:text-[var(--color-content-default-primary)] active:border-[var(--color-border-default-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-invert-secondary)] disabled:text-[var(--color-content-default-tertiary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100",
outline:
"bg-transparent text-[var(--color-content-default-primary)] border-[1.5px] border-[var(--color-border-invert-primary)] hover:bg-transparent hover:text-[var(--color-content-default-brand-primary)] hover:border-[1.5px] hover:border-[var(--color-border-default-brand-primary)] hover:scale-[1.02] focus:bg-transparent focus:text-[var(--color-content-default-primary)] focus:outline-none focus:border-[1.5px] focus:border-[var(--color-border-invert-primary)] focus:shadow-[0_0_0px_2px_var(--color-border-default-primary),0_0_0px_4px_var(--color-border-invert-primary)] focus:scale-[1.02] active:bg-transparent active:text-[var(--color-content-default-primary)] active:border-[1.5px] active:border-[var(--color-border-default-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-invert-tertiary)] disabled:border-[1.5px] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100",
"bg-transparent text-[var(--color-content-default-primary)] border-[1.5px] border-[var(--color-border-invert-primary)] hover:bg-transparent hover:text-[var(--color-content-default-brand-primary)] hover:border-[1.5px] hover:border-[var(--color-border-default-brand-primary)] hover:scale-[1.02] focus-visible:bg-transparent focus-visible:text-[var(--color-content-default-primary)] focus-visible:outline-none focus-visible:border-[1.5px] focus-visible:border-[var(--color-border-invert-primary)] focus-visible:shadow-[0_0_0px_2px_var(--color-border-default-primary),0_0_0px_4px_var(--color-border-invert-primary)] focus-visible:scale-[1.02] active:bg-transparent active:text-[var(--color-content-default-primary)] active:border-[1.5px] active:border-[var(--color-border-default-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-invert-tertiary)] disabled:border-[1.5px] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100",
"outline-inverse":
"bg-transparent text-[var(--color-content-invert-primary)] border-[1.5px] border-[var(--color-border-default-primary)] hover:bg-transparent hover:text-[var(--color-content-invert-brand-primary)] hover:border-[1.5px] hover:border-[var(--color-border-invert-brand-primary)] hover:scale-[1.02] focus:bg-transparent focus:text-[var(--color-content-invert-primary)] focus:outline-none focus:border-[1.5px] focus:border-[var(--color-border-default-primary)] focus:shadow-[0_0_0px_2px_var(--color-border-invert-primary),0_0_0px_4px_var(--color-border-default-primary)] focus:scale-[1.02] active:bg-transparent active:text-[var(--color-content-invert-primary)] active:border-[1.5px] active:border-[var(--color-border-invert-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-invert-secondary)] disabled:text-[var(--color-content-default-tertiary)] disabled:border-[1.5px] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100",
"bg-transparent text-[var(--color-content-invert-primary)] border-[1.5px] border-[var(--color-border-default-primary)] hover:bg-transparent hover:text-[var(--color-content-invert-brand-primary)] hover:border-[1.5px] hover:border-[var(--color-border-invert-brand-primary)] hover:scale-[1.02] focus-visible:bg-transparent focus-visible:text-[var(--color-content-invert-primary)] focus-visible:outline-none focus-visible:border-[1.5px] focus-visible:border-[var(--color-border-default-primary)] focus-visible:shadow-[0_0_0px_2px_var(--color-border-invert-primary),0_0_0px_4px_var(--color-border-default-primary)] focus-visible:scale-[1.02] active:bg-transparent active:text-[var(--color-content-invert-primary)] active:border-[1.5px] active:border-[var(--color-border-invert-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-invert-secondary)] disabled:text-[var(--color-content-default-tertiary)] disabled:border-[1.5px] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100",
ghost:
"bg-transparent text-[var(--color-content-default-brand-primary)] border-[1.5px] border-transparent hover:bg-transparent hover:text-[var(--color-content-default-primary)] hover:border-transparent hover:scale-[1.02] focus:bg-transparent focus:text-[var(--color-content-default-brand-primary)] focus:outline-none focus:border-transparent focus:shadow-[0_0_0px_2px_var(--color-border-default-primary),0_0_0px_4px_var(--color-border-invert-primary)] focus:scale-[1.02] active:bg-transparent active:text-[var(--color-content-default-primary)] active:border-[var(--color-border-default-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-invert-tertiary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100",
"bg-transparent text-[var(--color-content-default-brand-primary)] border-[1.5px] border-transparent hover:bg-transparent hover:text-[var(--color-content-default-primary)] hover:border-transparent hover:scale-[1.02] focus-visible:bg-transparent focus-visible:text-[var(--color-content-default-brand-primary)] focus-visible:outline-none focus-visible:border-transparent focus-visible:shadow-[0_0_0px_2px_var(--color-border-default-primary),0_0_0px_4px_var(--color-border-invert-primary)] focus-visible:scale-[1.02] active:bg-transparent active:text-[var(--color-content-default-primary)] active:border-[var(--color-border-default-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-invert-tertiary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100",
"ghost-inverse":
"bg-transparent text-[var(--color-content-invert-primary)] border-[1.5px] border-transparent hover:bg-transparent hover:text-[var(--color-content-invert-primary)] hover:border-transparent hover:scale-[1.02] focus:bg-transparent focus:text-[var(--color-content-invert-brand-primary)] focus:outline-none focus:border-transparent focus:shadow-[0_0_0px_2px_var(--color-border-invert-primary),0_0_0px_4px_var(--color-border-default-primary)] focus:scale-[1.02] active:bg-[var(--color-surface-invert-brand-primary)] active:text-[var(--color-content-invert-primary)] active:border-[var(--color-border-invert-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-invert-secondary)] disabled:text-[var(--color-content-default-tertiary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100",
"bg-transparent text-[var(--color-content-invert-primary)] border-[1.5px] border-transparent hover:bg-transparent hover:text-[var(--color-content-invert-primary)] hover:border-transparent hover:scale-[1.02] focus-visible:bg-transparent focus-visible:text-[var(--color-content-invert-brand-primary)] focus-visible:outline-none focus-visible:border-transparent focus-visible:shadow-[0_0_0px_2px_var(--color-border-invert-primary),0_0_0px_4px_var(--color-border-default-primary)] focus-visible:scale-[1.02] active:bg-[var(--color-surface-invert-brand-primary)] active:text-[var(--color-content-invert-primary)] active:border-[var(--color-border-invert-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-invert-secondary)] disabled:text-[var(--color-content-default-tertiary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100",
danger:
"bg-transparent text-[var(--color-border-default-negative-primary)] border border-[var(--color-border-default-negative-primary)] hover:bg-[var(--color-surface-invert-negative-secondary)] hover:text-[var(--color-border-default-negative-primary)] hover:border-[var(--color-border-default-negative-primary)] hover:scale-[1.02] focus:bg-transparent focus:text-[var(--color-border-default-negative-primary)] focus:outline-none focus:border-[var(--color-border-default-negative-primary)] focus:shadow-[0_0_0px_2px_var(--color-border-default-primary),0_0_0px_4px_var(--color-border-invert-primary)] focus:scale-[1.02] active:bg-[var(--color-surface-invert-negative-primary)] active:text-[var(--color-content-invert-negative-primary)] active:border-[1.5px] active:border-[var(--color-border-default-negative-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-inverse-tertiary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100",
"bg-transparent text-[var(--color-border-default-negative-primary)] border border-[var(--color-border-default-negative-primary)] hover:bg-[var(--color-surface-invert-negative-secondary)] hover:text-[var(--color-border-default-negative-primary)] hover:border-[var(--color-border-default-negative-primary)] hover:scale-[1.02] focus-visible:bg-transparent focus-visible:text-[var(--color-border-default-negative-primary)] focus-visible:outline-none focus-visible:border-[var(--color-border-default-negative-primary)] focus-visible:shadow-[0_0_0px_2px_var(--color-border-default-primary),0_0_0px_4px_var(--color-border-invert-primary)] focus-visible:scale-[1.02] active:bg-[var(--color-surface-invert-negative-primary)] active:text-[var(--color-content-invert-negative-primary)] active:border-[1.5px] active:border-[var(--color-border-default-negative-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-inverse-tertiary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100",
"danger-inverse":
"bg-transparent text-[var(--color-content-invert-negative-primary)] border border-[var(--color-border-invert-negative-primary)] hover:bg-transparent hover:text-[var(--color-content-invert-negative-primary)] hover:border-[var(--color-border-invert-negative-primary)] hover:scale-[1.02] focus:bg-transparent focus:text-[var(--color-content-invert-negative-primary)] focus:outline-none focus:border-[var(--color-border-invert-negative-primary)] focus:shadow-[0_0_0px_2px_var(--color-border-invert-primary),0_0_0px_4px_var(--color-border-default-primary)] focus:scale-[1.02] active:bg-[var(--color-surface-default-negative-primary)] active:text-[var(--color-content-default-primary)] active:border-[1.5px] active:border-[var(--color-border-default-negative-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-inverse-secondary)] disabled:text-[var(--color-content-default-tertiary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100",
"bg-transparent text-[var(--color-content-invert-negative-primary)] border border-[var(--color-border-invert-negative-primary)] hover:bg-transparent hover:text-[var(--color-content-invert-negative-primary)] hover:border-[var(--color-border-invert-negative-primary)] hover:scale-[1.02] focus-visible:bg-transparent focus-visible:text-[var(--color-content-invert-negative-primary)] focus-visible:outline-none focus-visible:border-[var(--color-border-invert-negative-primary)] focus-visible:shadow-[0_0_0px_2px_var(--color-border-invert-primary),0_0_0px_4px_var(--color-border-default-primary)] focus-visible:scale-[1.02] active:bg-[var(--color-surface-default-negative-primary)] active:text-[var(--color-content-default-primary)] active:border-[1.5px] active:border-[var(--color-border-default-negative-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-inverse-secondary)] disabled:text-[var(--color-content-default-tertiary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100",
};
const hoverOutlineStyles: Record<string, string> = {
@@ -0,0 +1,17 @@
"use client";
/**
* Figma: Community Rule System — "Add Custom Field/Popover" (List-item/lockup)
* https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=20887-175710
*/
import { memo } from "react";
import { ListItemView } from "./ListItem.view";
import type { ListItemProps } from "./ListItem.types";
const ListItem = memo<ListItemProps>((props) => {
return <ListItemView {...props} />;
});
ListItem.displayName = "ListItem";
export default ListItem;
@@ -0,0 +1,10 @@
import type { IconName } from "../../asset/icon";
export type ListItemProps = {
label: string;
leadingIcon: IconName;
onClick: () => void;
/** Bottom divider between rows — false on the final row per Figma. */
showDivider: boolean;
className?: string;
};
@@ -0,0 +1,35 @@
"use client";
import { memo } from "react";
import Icon from "../../asset/icon";
import type { ListItemProps } from "./ListItem.types";
export const ListItemView = memo(function ListItemView({
label,
leadingIcon,
onClick,
showDivider,
className = "",
}: ListItemProps) {
const dividerClass = showDivider
? "border-b border-solid border-[var(--color-border-default-tertiary)]"
: "";
return (
<button
type="button"
role="menuitem"
onClick={onClick}
className={`relative flex w-full shrink-0 cursor-pointer items-center gap-[6px] px-[4px] py-[16px] text-left hover:bg-[var(--color-surface-default-tertiary)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-border-invert-primary)] ${dividerClass} ${className}`}
>
<span className="flex size-6 shrink-0 items-center justify-center overflow-visible text-[var(--color-content-default-primary)]">
<Icon name={leadingIcon} size={24} />
</span>
<span className="min-w-0 flex-1 text-left font-inter text-[12px] font-normal leading-4 whitespace-normal text-[var(--color-content-default-primary)]">
{label}
</span>
</button>
);
});
ListItemView.displayName = "ListItemView";
+2
View File
@@ -0,0 +1,2 @@
export { default } from "./ListItem.container";
export type { ListItemProps } from "./ListItem.types";
@@ -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";
@@ -2,12 +2,14 @@
import { memo } from "react";
import { useRouter } from "next/navigation";
import { useTranslation } from "../../../contexts/MessagesContext";
import { CreateFlowTopNavView } from "./CreateFlowTopNav.view";
import type { CreateFlowTopNavProps } from "./CreateFlowTopNav.types";
/**
* Figma: Utility / CreateFlowTopNav — wizard header (create-flow chrome).
* Exit, optional share / export / edit; strings in `messages/en/create/topNav.json`.
* Export menu: Community Rule System — node 21998:22612 (`messages/en/modals/popoverExport.json`).
*/
const CreateFlowTopNavContainer = memo<CreateFlowTopNavProps>(
({
@@ -16,13 +18,14 @@ const CreateFlowTopNavContainer = memo<CreateFlowTopNavProps>(
hasEdit = false,
saveDraftOnExit = false,
onShare,
onExport,
onSelectExportFormat,
onEdit,
onExit,
buttonPalette,
className = "",
}) => {
const router = useRouter();
const tPopover = useTranslation("modals.popoverExport");
const handleExit = (options?: { saveDraft?: boolean }) => {
if (onExit) {
@@ -40,11 +43,15 @@ const CreateFlowTopNavContainer = memo<CreateFlowTopNavProps>(
hasEdit={hasEdit}
saveDraftOnExit={saveDraftOnExit}
onShare={onShare}
onExport={onExport}
onSelectExportFormat={onSelectExportFormat}
onEdit={onEdit}
onExit={handleExit}
buttonPalette={buttonPalette}
className={className}
exportPopoverMenuAriaLabel={tPopover("menuAriaLabel")}
exportPopoverPdfLabel={tPopover("downloadPdf")}
exportPopoverCsvLabel={tPopover("downloadCsv")}
exportPopoverMarkdownLabel={tPopover("downloadMarkdown")}
/>
);
},
@@ -32,9 +32,9 @@ export interface CreateFlowTopNavProps {
*/
onShare?: () => void;
/**
* Callback when Export button is clicked
* Callback when user picks an export format from the Export menu.
*/
onExport?: () => void;
onSelectExportFormat?: (_format: "pdf" | "csv" | "markdown") => void;
/**
* Callback when Edit button is clicked
*/
@@ -54,3 +54,11 @@ export interface CreateFlowTopNavProps {
*/
className?: string;
}
/** Resolved copy for the export popover; supplied by the container. */
export type CreateFlowTopNavViewProps = CreateFlowTopNavProps & {
exportPopoverMenuAriaLabel: string;
exportPopoverPdfLabel: string;
exportPopoverCsvLabel: string;
exportPopoverMarkdownLabel: string;
};
@@ -1,9 +1,12 @@
"use client";
import { useEffect, useId, useRef, useState } from "react";
import Logo from "../../asset/Logo";
import Button from "../../buttons/Button";
import ListItem from "../../layout/ListItem";
import Popover from "../../modals/Popover";
import { useTranslation } from "../../../contexts/MessagesContext";
import type { CreateFlowTopNavProps } from "./CreateFlowTopNav.types";
import type { CreateFlowTopNavViewProps } from "./CreateFlowTopNav.types";
const exitButtonFigmaClass =
"!rounded-[var(--radius-measures-radius-full,9999px)] !border-[1.25px] !px-[var(--spacing-measures-spacing-250,10px)] !py-[var(--spacing-measures-spacing-200,8px)] md:!text-[12px] md:!leading-[14px]";
@@ -14,14 +17,44 @@ export function CreateFlowTopNavView({
hasEdit = false,
saveDraftOnExit = false,
onShare,
onExport,
onSelectExportFormat,
onEdit,
onExit,
buttonPalette = "default",
className = "",
}: CreateFlowTopNavProps) {
exportPopoverMenuAriaLabel,
exportPopoverPdfLabel,
exportPopoverCsvLabel,
exportPopoverMarkdownLabel,
}: CreateFlowTopNavViewProps) {
const t = useTranslation("create.topNav");
const exitButtonText = saveDraftOnExit ? t("saveAndExit") : t("exit");
const [exportMenuOpen, setExportMenuOpen] = useState(false);
const exportWrapRef = useRef<HTMLDivElement>(null);
const exportMenuId = useId();
useEffect(() => {
if (!exportMenuOpen) return;
const onDoc = (e: MouseEvent) => {
if (
exportWrapRef.current &&
!exportWrapRef.current.contains(e.target as Node)
) {
setExportMenuOpen(false);
}
};
document.addEventListener("mousedown", onDoc);
return () => document.removeEventListener("mousedown", onDoc);
}, [exportMenuOpen]);
useEffect(() => {
if (!exportMenuOpen) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") setExportMenuOpen(false);
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [exportMenuOpen]);
return (
<header
@@ -50,32 +83,74 @@ export function CreateFlowTopNavView({
</Button>
)}
{hasExport && (
<Button
buttonType="outline"
palette={buttonPalette}
size="xsmall"
onClick={onExport}
ariaLabel={t("exportAriaLabel")}
className="justify-center gap-[var(--spacing-scale-002,2px)] !pl-[var(--spacing-scale-012,12px)] !pr-[var(--spacing-scale-006,6px)] md:!pr-[var(--spacing-scale-006,6px)] !text-[10px] md:!text-[12px] !leading-[12px] md:!leading-[14px] !py-[6px] md:!py-[8px] !border md:!border-[1.5px]"
>
<span>{t("export")}</span>
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
className="shrink-0 md:w-[14px] md:h-[14px]"
aria-hidden="true"
{hasExport && onSelectExportFormat ? (
<div className="relative" ref={exportWrapRef}>
<Button
buttonType="outline"
palette={buttonPalette}
size="xsmall"
type="button"
ariaLabel={t("exportAriaLabel")}
aria-haspopup="menu"
aria-expanded={exportMenuOpen}
aria-controls={exportMenuId}
onClick={() => setExportMenuOpen((o) => !o)}
className="justify-center gap-[var(--spacing-scale-002,2px)] !pl-[var(--spacing-scale-012,12px)] !pr-[var(--spacing-scale-006,6px)] md:!pr-[var(--spacing-scale-006,6px)] !text-[10px] md:!text-[12px] !leading-[12px] md:!leading-[14px] !py-[6px] md:!py-[8px] !border md:!border-[1.5px]"
>
<path d="M19 9l-7 7-7-7" />
</svg>
</Button>
)}
<span>{t("export")}</span>
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
className="shrink-0 md:w-[14px] md:h-[14px]"
aria-hidden="true"
>
<path d="M19 9l-7 7-7-7" />
</svg>
</Button>
{exportMenuOpen ? (
<div className="absolute right-0 top-[calc(100%+var(--spacing-measures-spacing-200,8px))] z-[300]">
<Popover
id={exportMenuId}
menuAriaLabel={exportPopoverMenuAriaLabel}
>
<ListItem
showDivider
leadingIcon="picture_as_pdf"
label={exportPopoverPdfLabel}
onClick={() => {
onSelectExportFormat("pdf");
setExportMenuOpen(false);
}}
/>
<ListItem
showDivider
leadingIcon="csv"
label={exportPopoverCsvLabel}
onClick={() => {
onSelectExportFormat("csv");
setExportMenuOpen(false);
}}
/>
<ListItem
showDivider={false}
leadingIcon="markdown_copy"
label={exportPopoverMarkdownLabel}
onClick={() => {
onSelectExportFormat("markdown");
setExportMenuOpen(false);
}}
/>
</Popover>
</div>
) : null}
</div>
) : null}
{hasEdit && (
<Button
@@ -106,3 +181,5 @@ export function CreateFlowTopNavView({
</header>
);
}
CreateFlowTopNavView.displayName = "CreateFlowTopNavView";