Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ packages/react-components/FIGMA_MAPPING_RULES.md
packages/react-components/src/shadcn/components/ui/input+desc.tsx
.vscode/settings.json
packages/react-components/src/aidbox-ui.code-workspace
COMPONENT_PATTERNS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type RequestMethod = (typeof METHODS)[number];

const requestMethodVariants = cva<{ method: { [K in RequestMethod]: string } }>(
cn(
"cursor-pointer",
"border-r-0",
"rounded-r-none",
"shadow-none",
Expand Down
109 changes: 109 additions & 0 deletions packages/react-components/src/components/sandbox.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { Check, Copy } from "lucide-react";
import { Sandbox } from "./sandbox";

const meta = {
title: "Component/Sandbox",
component: Sandbox,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
argTypes: {
url: {
control: "text",
description: "Display url",
},
showCopy: {
control: "boolean",
description: "Show the copy button",
},
copyIcon: {
control: false,
description: "Show the copy icon",
},
tooltipText: {
control: "text",
description: "Copy button tooltip text",
},
showToast: {
control: "boolean",
description: "Show toast on copy",
},
onCopy: {
action: "copied",
description: "Successful copy callback",
},
},
};

export default meta;

export const Default = {
args: {
url: "http://localhost:8080/fhir",
showCopy: true,
copyIcon: <Copy />,
tooltipText: "Copy URL",
showToast: true,
},
};

export const LongUrl = {
args: {
url: "https://very-long-domain-name-that-should-be-truncated.example.com/api/v1/very-long-endpoint-name",
showCopy: true,
copyIcon: <Copy />,
tooltipText: "Copy URL",
showToast: true,
},
};

export const WithoutCopy = {
args: {
url: "http://localhost:8080/fhir",
showCopy: false,
},
};

export const CustomIcon = {
args: {
url: "http://localhost:8080/fhir",
showCopy: true,
copyIcon: <Check />,
tooltipText: "Copy URL",
showToast: true,
},
};

export const CustomTooltip = {
args: {
url: "http://localhost:8080/fhir",
showCopy: true,
copyIcon: <Copy />,
tooltipText: "Copy link",
showToast: true,
},
};

export const WithoutToast = {
args: {
url: "http://localhost:8080/fhir",
showCopy: true,
copyIcon: <Copy />,
tooltipText: "Copy URL",
showToast: false,
},
};

export const WithCallback = {
args: {
url: "http://localhost:8080/fhir",
showCopy: true,
copyIcon: <Copy />,
tooltipText: "Copy URL",
showToast: true,
onCopy: (text: string) => {
console.log("Copied:", text);
},
},
};
121 changes: 121 additions & 0 deletions packages/react-components/src/components/sandbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { Copy } from "lucide-react";
import type React from "react";
import { toast } from "sonner";
import { Toaster } from "../shadcn/components/ui/sonner";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "../shadcn/components/ui/tooltip";
import { cn } from "../shadcn/lib/utils";

const baseSandboxStyles = cn(
"flex",
"flex-col",
"items-start",
"relative",
"size-full",
"gap-2",
);

const inputContainerStyles = cn(
"flex",
"items-center",
"gap-2",
"w-full",
"px-3",
"py-2",
"bg-bg-tertiary",
"rounded",
"overflow-hidden",
);

const contentStyles = cn("flex", "items-center", "gap-2", "flex-1", "min-w-0");

const textStyles = cn(
"typo-body",
"text-text-primary",
"flex-1",
"min-w-0",
"truncate",
);

const copyButtonStyles = cn(
"flex",
"items-center",
"justify-center",
"shrink-0",
"size-4",
"hover:opacity-80",
"transition-opacity",
"duration-200",
);

export type SandboxProps = {
url: string;
showCopy?: boolean;
copyIcon?: React.ReactNode;
tooltipText?: string;
showToast?: boolean;
onCopy?: (text: string) => void;
} & Omit<React.ComponentProps<"div">, "children" | "onCopy">;

export function Sandbox({
url,
showCopy = true,
copyIcon = <Copy />,
tooltipText = "Copy URL",
showToast = true,
onCopy,
className,
...props
}: SandboxProps) {
return (
<>
<div
data-slot="sandbox"
className={cn(baseSandboxStyles, className)}
{...props}
>
<div className={inputContainerStyles}>
<div className={contentStyles}>
<span className={textStyles}>{url}</span>
</div>
{showCopy && (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className={copyButtonStyles}
onClick={async () => {
try {
await navigator.clipboard.writeText(url);

if (showToast) {
toast("Copied", {
description:
url.length > 50 ? `${url.slice(0, 50)}...` : url,
duration: 2000,
});
}

onCopy?.(url);
} catch (error) {
console.error("Failed to copy to clipboard:", error);
}
}}
>
{copyIcon}
</button>
</TooltipTrigger>
<TooltipContent>
<p>{tooltipText}</p>
</TooltipContent>
</Tooltip>
)}
</div>
</div>
<Toaster position="top-center" />
</>
);
}
Loading