"use client"; import { useEffect, useState } from "react"; import { AnimatePresence, motion } from "motion/react"; import { Doc, Palette } from "../lib/tokens"; import { shareLink } from "../lib/share"; import { Icon } from "./M3Node"; import { t, useLang } from "../lib/i18n"; const BASE = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; /** the app's own URL without any hash, and the agent guide beside it */ const appUrl = () => `${window.location.origin}${BASE}/`; const guideUrl = () => `${appUrl()}agent.md`; /** the text button that opens the dialog: a label and a beta badge, no icon; busy while a model drafts */ export function ShareButton({ p, onClick, busy }: { p: Palette; onClick: () => void; busy?: boolean }) { const lang = useLang(); return ( ); } const Beta = ({ p }: { p: Palette }) => ( BETA ); /** Ask an AI (beta): write the idea and have the author's own model draft it, or copy the * instruction for a coding agent. The title row carries a link to the design as it is now. */ export function ShareDialog({ p, doc, aiReady, idea, onIdea, open, onClose, onDraft, onSetupAi, }: { p: Palette; doc: Doc; aiReady: boolean; /** what to build, kept by the page so a failed draft does not lose it */ idea: string; onIdea: (v: string) => void; open: boolean; onClose: () => void; /** the idea, for the author's own model to draft */ onDraft: (idea: string) => void; /** opens the AI settings so a key can be entered */ onSetupAi: () => void; }) { const lang = useLang(); const [copied, setCopied] = useState<"ask" | "link" | null>(null); useEffect(() => { if (!copied) return; const id = setTimeout(() => setCopied(null), copied === "ask" ? 4000 : 1400); return () => clearTimeout(id); }, [copied]); /* a fresh dialog starts without a "copied" mark; kept apart from the key handler, whose onClose changes identity on every render of the page */ useEffect(() => { if (open) setCopied(null); }, [open]); useEffect(() => { if (!open) return; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") { e.stopPropagation(); onClose(); } }; window.addEventListener("keydown", onKey, true); return () => window.removeEventListener("keydown", onKey, true); }, [open, onClose]); const copyAsk = async () => { const text = t("askAiText", lang).replace("{url}", guideUrl()).replace("{idea}", idea.trim() || t("askAiIdeaFallback", lang)); try { await navigator.clipboard.writeText(text); setCopied("ask"); } catch {} }; const copyLink = async () => { try { await navigator.clipboard.writeText(await shareLink(doc, appUrl())); setCopied("link"); } catch {} }; /* a connected pair, the way the canvas draws connected buttons: outer corners round, inner ones tight */ const pill = (icon: string, label: string, onClick: () => void, opts?: { primary?: boolean; disabled?: boolean; corners?: "left" | "right"; title?: string }) => { const outer = 20; const inner = 8; const radius = opts?.corners === "left" ? `${outer}px ${inner}px ${inner}px ${outer}px` : opts?.corners === "right" ? `${inner}px ${outer}px ${outer}px ${inner}px` : outer; return ( ); }; return ( {open && ( e.stopPropagation()} style={{ width: "min(100%, 620px)", padding: 24, borderRadius: 28, background: p.surfaceContainerHigh, color: p.onSurface, boxShadow: "0 8px 24px rgba(0,0,0,0.18), 0 2px 6px rgba(0,0,0,0.10)", display: "flex", flexDirection: "column", gap: 14, }} >
{t("askAi", lang)} {pill(copied === "link" ? "check" : "link", copied === "link" ? t("copied", lang) : t("shareLinkCopy", lang), copyLink, { title: t("shareLinkHint", lang) })}

{t("askAiHint", lang)}