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
@@ -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";