diff --git a/app/demo/share-dialog/page.tsx b/app/demo/share-dialog/page.tsx
new file mode 100644
index 0000000..97da603
--- /dev/null
+++ b/app/demo/share-dialog/page.tsx
@@ -0,0 +1,151 @@
+"use client";
+
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { ShareDialog } from "@/components/share-dialog";
+
+export default function ShareDialogDemo() {
+ const [dialogOpen, setDialogOpen] = useState(false);
+ const [scenario, setScenario] = useState<
+ "simple" | "with-title" | "long-key"
+ >("simple");
+
+ const scenarios = {
+ simple: {
+ url: "https://ghostpaste.dev/g/abc123#key=eyJhbGciOiJBMTI4R0NNIiwiZXhwIjoxNjg5MzM2MDAwfQ",
+ title: undefined,
+ },
+ "with-title": {
+ url: "https://ghostpaste.dev/g/def456#key=eyJhbGciOiJBMTI4R0NNIiwiZXhwIjoxNjg5MzM2MDAwfQ",
+ title: "My Configuration Files",
+ },
+ "long-key": {
+ url: "https://ghostpaste.dev/g/xyz789#key=eyJhbGciOiJBMTI4R0NNIiwiZXhwIjoxNjg5MzM2MDAwLCJpdiI6IjEyMzQ1Njc4OTBhYmNkZWYiLCJkYXRhIjoiZXlKaGJHY2lPaUZCTVRJNFIwTk5JaXdpWlhod0lqb3hOamc1TXpNMk1EQXdNSDAifQ",
+ title: "React Component Library",
+ },
+ };
+
+ const handleCopy = () => {
+ console.log("URL copied to clipboard");
+ };
+
+ const handleDownload = () => {
+ console.log("Text file downloaded");
+ };
+
+ return (
+
+
+
+
+ ShareDialog Demo
+
+
+ Demo of the ShareDialog component with different scenarios.
+
+
+
+
+
Test Scenarios
+
+
+
+
Simple Gist
+
+ Basic gist without title
+
+
+
+
+
+
Gist with Title
+
+ Gist with a descriptive title
+
+
+
+
+
+
Long Encryption Key
+
+ Gist with very long encryption key
+
+
+
+
+
+
+
+
Features Demonstrated
+
+
+
✅ Core Features
+
+ - • Visual URL fragment separation
+ - • Copy to clipboard with feedback
+ - • Download as text file
+ - • Security warning display
+ - • Success animation on creation
+ - • Mobile-friendly responsive layout
+
+
+
+
🔒 Security Features
+
+ - • Fragment key highlighting
+ - • Clear security warnings
+ - • Complete URL copy requirement
+ - • No URL logging or analytics
+ - • Fallback copy for older browsers
+
+
+
+
+
+
+
Accessibility Features
+
+ - • Keyboard navigation support
+ - • Focus trap within dialog
+ - • Escape key to close
+ - • Screen reader friendly labels
+ - • Auto-focus on primary action
+
+
+
+
+
+
+ );
+}
diff --git a/components/share-dialog.test.tsx b/components/share-dialog.test.tsx
new file mode 100644
index 0000000..bae17e3
--- /dev/null
+++ b/components/share-dialog.test.tsx
@@ -0,0 +1,263 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import { render, screen, fireEvent } from "@testing-library/react";
+import { ShareDialog } from "./share-dialog";
+
+// Mock clipboard API
+const mockWriteText = vi.fn();
+Object.defineProperty(global.navigator, "clipboard", {
+ value: { writeText: mockWriteText },
+ writable: true,
+ configurable: true,
+});
+
+// Mock document.execCommand for fallback
+Object.defineProperty(document, "execCommand", {
+ value: vi.fn(() => true),
+ writable: true,
+ configurable: true,
+});
+
+// Mock URL.createObjectURL and revokeObjectURL
+Object.defineProperty(global.URL, "createObjectURL", {
+ value: vi.fn(() => "mock-blob-url"),
+ writable: true,
+});
+
+Object.defineProperty(global.URL, "revokeObjectURL", {
+ value: vi.fn(),
+ writable: true,
+});
+
+describe("ShareDialog", () => {
+ const defaultProps = {
+ open: true,
+ onOpenChange: vi.fn(),
+ shareUrl: "https://ghostpaste.dev/g/abc123#key=eyJhbGciOiJBMTI4R0NNIn0",
+ };
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ // Clear any existing timeouts
+ vi.clearAllTimers();
+ vi.useFakeTimers();
+ });
+
+ afterEach(() => {
+ vi.runOnlyPendingTimers();
+ vi.useRealTimers();
+ });
+
+ it("renders when open", () => {
+ render();
+
+ expect(screen.getByText("Gist Created Successfully!")).toBeInTheDocument();
+ expect(screen.getByText("Your secure link:")).toBeInTheDocument();
+ });
+
+ it("does not render when closed", () => {
+ render();
+
+ expect(
+ screen.queryByText("Gist Created Successfully!")
+ ).not.toBeInTheDocument();
+ });
+
+ it("displays URL with fragment separation", () => {
+ render();
+
+ // Base URL should be visible
+ expect(
+ screen.getByText("https://ghostpaste.dev/g/abc123")
+ ).toBeInTheDocument();
+ // Fragment should be visible with special styling
+ expect(
+ screen.getByText("#key=eyJhbGciOiJBMTI4R0NNIn0")
+ ).toBeInTheDocument();
+ });
+
+ it("displays security warning", () => {
+ render();
+
+ expect(screen.getByText("Important Security Notice")).toBeInTheDocument();
+ expect(screen.getByText(/The blue part after/)).toBeInTheDocument();
+ expect(screen.getByText(/complete URL/)).toBeInTheDocument();
+ });
+
+ it("calls copy function when copy button is clicked", () => {
+ mockWriteText.mockResolvedValue(undefined);
+ const onCopy = vi.fn();
+
+ render();
+
+ const copyButton = screen.getByLabelText("Copy URL to clipboard");
+ fireEvent.click(copyButton);
+
+ expect(mockWriteText).toHaveBeenCalledWith(defaultProps.shareUrl);
+ });
+
+ it("shows copy button initially", () => {
+ render();
+
+ expect(screen.getByText("Copy Link")).toBeInTheDocument();
+ });
+
+ it("attempts fallback when clipboard API fails", () => {
+ mockWriteText.mockRejectedValue(new Error("Clipboard not available"));
+
+ render();
+
+ const copyButton = screen.getByLabelText("Copy URL to clipboard");
+ fireEvent.click(copyButton);
+
+ // Should attempt the clipboard API first
+ expect(mockWriteText).toHaveBeenCalledWith(defaultProps.shareUrl);
+ });
+
+ it("downloads text file when download button is clicked", () => {
+ const onDownload = vi.fn();
+ const createElementSpy = vi.spyOn(document, "createElement");
+ const appendChildSpy = vi.spyOn(document.body, "appendChild");
+ const removeChildSpy = vi.spyOn(document.body, "removeChild");
+
+ render();
+
+ const downloadButton = screen.getByText("Download as Text");
+ fireEvent.click(downloadButton);
+
+ expect(createElementSpy).toHaveBeenCalledWith("a");
+ expect(appendChildSpy).toHaveBeenCalled();
+ expect(removeChildSpy).toHaveBeenCalled();
+ expect(onDownload).toHaveBeenCalled();
+ expect(global.URL.createObjectURL).toHaveBeenCalled();
+ expect(global.URL.revokeObjectURL).toHaveBeenCalled();
+ });
+
+ it("includes gist title in download content when provided", () => {
+ const gistTitle = "My Test Gist";
+ let blobContent = "";
+
+ // Mock Blob to capture content
+ const originalBlob = global.Blob;
+ global.Blob = vi.fn().mockImplementation((content) => {
+ blobContent = content[0];
+ return new originalBlob(content);
+ }) as unknown as typeof Blob;
+
+ render();
+
+ const downloadButton = screen.getByText("Download as Text");
+ fireEvent.click(downloadButton);
+
+ expect(blobContent).toContain(`Title: ${gistTitle}`);
+ expect(blobContent).toContain(defaultProps.shareUrl);
+ expect(blobContent).toContain(
+ "IMPORTANT: This URL contains a decryption key"
+ );
+
+ // Restore original Blob
+ global.Blob = originalBlob;
+ });
+
+ it("calls onOpenChange when dialog is closed", () => {
+ const onOpenChange = vi.fn();
+
+ render();
+
+ const doneButton = screen.getByText("Done");
+ fireEvent.click(doneButton);
+
+ expect(onOpenChange).toHaveBeenCalledWith(false);
+ });
+
+ it("handles URLs without fragments", () => {
+ const urlWithoutFragment = "https://ghostpaste.dev/g/abc123";
+
+ render();
+
+ expect(
+ screen.getByText("https://ghostpaste.dev/g/abc123")
+ ).toBeInTheDocument();
+ // Should not render any blue fragment text (the # in security warning doesn't count)
+ expect(screen.queryByText(/^#key=/)).not.toBeInTheDocument();
+ });
+
+ it("has proper accessibility attributes", () => {
+ render();
+
+ // Dialog should have proper ARIA attributes
+ const dialog = screen.getByRole("dialog");
+ expect(dialog).toBeInTheDocument();
+
+ // Copy button should have aria-label
+ const copyButton = screen.getByLabelText("Copy URL to clipboard");
+ expect(copyButton).toBeInTheDocument();
+
+ // Close button should have screen reader text
+ expect(screen.getByText("Close")).toBeInTheDocument();
+ });
+
+ it("has copy button that can be clicked", () => {
+ mockWriteText.mockResolvedValue(undefined);
+
+ render();
+
+ const copyButton = screen.getByLabelText("Copy URL to clipboard");
+ expect(copyButton).toBeEnabled();
+ });
+
+ it("formats download filename correctly with title", () => {
+ let downloadAttribute = "";
+
+ // Mock anchor element creation
+ const originalCreateElement = document.createElement;
+ vi.spyOn(document, "createElement").mockImplementation((tagName) => {
+ if (tagName === "a") {
+ const element = originalCreateElement.call(document, tagName);
+ Object.defineProperty(element, "download", {
+ set: (value) => {
+ downloadAttribute = value;
+ },
+ get: () => downloadAttribute,
+ configurable: true,
+ });
+ return element;
+ }
+ return originalCreateElement.call(document, tagName);
+ });
+
+ render();
+
+ const downloadButton = screen.getByText("Download as Text");
+ fireEvent.click(downloadButton);
+
+ expect(downloadAttribute).toBe("ghostpaste-My-Test---Gist-.txt");
+ });
+
+ it("uses default filename when no title provided", () => {
+ let downloadAttribute = "";
+
+ // Mock anchor element creation
+ const originalCreateElement = document.createElement;
+ vi.spyOn(document, "createElement").mockImplementation((tagName) => {
+ if (tagName === "a") {
+ const element = originalCreateElement.call(document, tagName);
+ Object.defineProperty(element, "download", {
+ set: (value) => {
+ downloadAttribute = value;
+ },
+ get: () => downloadAttribute,
+ configurable: true,
+ });
+ return element;
+ }
+ return originalCreateElement.call(document, tagName);
+ });
+
+ render();
+
+ const downloadButton = screen.getByText("Download as Text");
+ fireEvent.click(downloadButton);
+
+ expect(downloadAttribute).toBe("ghostpaste-gist.txt");
+ });
+});
diff --git a/components/share-dialog.tsx b/components/share-dialog.tsx
new file mode 100644
index 0000000..1067d4c
--- /dev/null
+++ b/components/share-dialog.tsx
@@ -0,0 +1,206 @@
+"use client";
+
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Check, Copy, Download, AlertTriangle } from "lucide-react";
+import { cn } from "@/lib/utils";
+
+export interface ShareDialogProps {
+ /** Whether the dialog is open */
+ open: boolean;
+ /** Callback when dialog open state changes */
+ onOpenChange: (open: boolean) => void;
+ /** The complete shareable URL including fragment */
+ shareUrl: string;
+ /** Optional title for the gist */
+ gistTitle?: string;
+ /** Optional callback when URL is copied */
+ onCopy?: () => void;
+ /** Optional callback when text file is downloaded */
+ onDownload?: () => void;
+}
+
+export function ShareDialog({
+ open,
+ onOpenChange,
+ shareUrl,
+ gistTitle,
+ onCopy,
+ onDownload,
+}: ShareDialogProps) {
+ const [copySuccess, setCopySuccess] = useState(false);
+
+ // Split the URL at the fragment for visual display
+ const urlParts = shareUrl.split("#");
+ const baseUrl = urlParts[0];
+ const fragment = urlParts[1] ? `#${urlParts[1]}` : "";
+
+ const handleCopy = async () => {
+ try {
+ await navigator.clipboard.writeText(shareUrl);
+ setCopySuccess(true);
+ onCopy?.();
+
+ // Reset success state after 2 seconds
+ setTimeout(() => setCopySuccess(false), 2000);
+ } catch (error) {
+ console.error("Failed to copy URL:", error);
+ // Fallback for older browsers
+ fallbackCopy();
+ }
+ };
+
+ const fallbackCopy = () => {
+ const textArea = document.createElement("textarea");
+ textArea.value = shareUrl;
+ textArea.style.position = "absolute";
+ textArea.style.left = "-999999px";
+ document.body.appendChild(textArea);
+ textArea.select();
+
+ try {
+ document.execCommand("copy");
+ setCopySuccess(true);
+ onCopy?.();
+ setTimeout(() => setCopySuccess(false), 2000);
+ } catch (error) {
+ console.error("Fallback copy failed:", error);
+ } finally {
+ document.body.removeChild(textArea);
+ }
+ };
+
+ const handleDownload = () => {
+ const content = `GhostPaste - Secure Gist\n\n${gistTitle ? `Title: ${gistTitle}\n` : ""}URL: ${shareUrl}\n\nIMPORTANT: This URL contains a decryption key in the fragment (after #).\nShare the complete URL to allow others to view the gist.\n\nCreated: ${new Date().toLocaleString()}`;
+
+ const blob = new Blob([content], { type: "text/plain" });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = `ghostpaste-${gistTitle ? gistTitle.replace(/[^a-zA-Z0-9]/g, "-") : "gist"}.txt`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+
+ onDownload?.();
+ };
+
+ return (
+
+ );
+}
diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx
new file mode 100644
index 0000000..584f7c7
--- /dev/null
+++ b/components/ui/dialog.tsx
@@ -0,0 +1,143 @@
+"use client";
+
+import * as React from "react";
+import * as DialogPrimitive from "@radix-ui/react-dialog";
+import { XIcon } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+
+function Dialog({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function DialogTrigger({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function DialogPortal({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function DialogClose({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function DialogOverlay({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function DialogContent({
+ className,
+ children,
+ showCloseButton = true,
+ ...props
+}: React.ComponentProps & {
+ showCloseButton?: boolean;
+}) {
+ return (
+
+
+
+ {children}
+ {showCloseButton && (
+
+
+ Close
+
+ )}
+
+
+ );
+}
+
+function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function DialogTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function DialogDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+export {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogOverlay,
+ DialogPortal,
+ DialogTitle,
+ DialogTrigger,
+};