feat: separate code snippets and code blocks for copyable snippets

This commit is contained in:
Aarnav Tale
2026-03-26 12:57:30 -04:00
parent a1eb6d35db
commit 30bbcbab2a
5 changed files with 48 additions and 83 deletions
+34
View File
@@ -0,0 +1,34 @@
import { Copy } from "lucide-react";
import cn from "~/utils/cn";
import toast from "~/utils/toast";
export interface CodeBlockProps {
children: string;
className?: string;
}
export default function CodeBlock({ children, className }: CodeBlockProps) {
const text = children.trim();
return (
<button
type="button"
className={cn(
"w-full cursor-pointer rounded-md bg-mist-100 text-left dark:bg-mist-800",
"hover:bg-mist-200 dark:hover:bg-mist-700 transition-colors",
className,
)}
onClick={async () => {
await navigator.clipboard.writeText(text);
toast("Copied to clipboard");
}}
>
<code className="block px-3 pt-2 pb-1 text-sm break-all">{text}</code>
<span className="mt-0.5 flex items-center gap-1 px-3 pb-2 text-xs text-mist-500 dark:text-mist-400">
<Copy className="size-3" />
Click to copy
</span>
</button>
);
}
+4 -36
View File
@@ -1,51 +1,19 @@
import { Check, Copy } from "lucide-react";
import { HTMLProps } from "react";
import cn from "~/utils/cn";
import toast from "~/utils/toast";
export interface CodeProps extends HTMLProps<HTMLSpanElement> {
isCopyable?: boolean;
export interface CodeProps {
children: string | string[] | number;
className?: string;
}
export default function Code({ isCopyable, children, className }: CodeProps) {
export default function Code({ children, className }: CodeProps) {
return (
<code
className={cn(
"bg-mist-100 dark:bg-mist-800 px-1 py-0.5 font-mono",
"rounded-sm focus-within:outline-hidden focus-within:ring-2",
isCopyable && "relative pr-7",
"bg-mist-100 dark:bg-mist-800 px-1.5 py-0.5 font-mono rounded-sm text-[0.875em]",
className,
)}
>
{children}
{isCopyable && (
<button
className="absolute right-0 bottom-0"
onClick={async (event) => {
const text = Array.isArray(children) ? children.join("") : String(children);
const svgs = event.currentTarget.querySelectorAll("svg");
for (const svg of svgs) {
svg.toggleAttribute("data-copied", true);
}
await navigator.clipboard.writeText(text);
toast("Copied to clipboard");
setTimeout(() => {
for (const svg of svgs) {
svg.toggleAttribute("data-copied", false);
}
}, 1000);
}}
type="button"
>
<Check className="hidden h-4.5 w-4.5 p-1 data-copied:block" />
<Copy className="block h-4.5 w-4.5 p-1 data-copied:hidden" />
</button>
)}
</code>
);
}
+3 -25
View File
@@ -1,4 +1,4 @@
import { Check, Copy } from "lucide-react";
import { Check } from "lucide-react";
import { redirect } from "react-router";
import androidSvg from "~/assets/android.svg";
@@ -6,14 +6,13 @@ import iosSvg from "~/assets/ios.svg";
import linuxSvg from "~/assets/linux.svg";
import macosSvg from "~/assets/macos.svg";
import windowsSvg from "~/assets/windows.svg";
import Button from "~/components/button";
import Card from "~/components/card";
import CodeBlock from "~/components/code-block";
import Link from "~/components/link";
import LinkAccount from "~/layout/link-account";
import { usersResource } from "~/server/headscale/live-store";
import { Capabilities } from "~/server/web/roles";
import cn from "~/utils/cn";
import toast from "~/utils/toast";
import { getUserDisplayName } from "~/utils/user";
import type { Route } from "./+types/home";
@@ -152,28 +151,7 @@ export default function Home({ loaderData }: Route.ComponentProps) {
<img alt="Linux" className="w-4 dark:invert" src={linuxSvg} />
<span className="text-sm font-medium">Linux</span>
</div>
<div className="mt-2 flex items-center gap-2">
<code
className={cn(
"flex-1 rounded-md px-3 py-2 text-xs h-8",
"bg-mist-100 dark:bg-mist-800",
)}
>
curl -fsSL https://tailscale.com/install.sh | sh
</code>
<Button
className="h-8 p-1 px-2"
variant="ghost"
onClick={async () => {
await navigator.clipboard.writeText(
"curl -fsSL https://tailscale.com/install.sh | sh",
);
toast("Copied to clipboard");
}}
>
<Copy className="size-4" />
</Button>
</div>
<CodeBlock className="mt-2">curl -fsSL https://tailscale.com/install.sh | sh</CodeBlock>
<p className="mt-1 text-xs text-mist-500 dark:text-mist-400">
<Link
external
+3 -5
View File
@@ -3,7 +3,7 @@ import { Computer, FileKey2 } from "lucide-react";
import { useState } from "react";
import { useNavigate } from "react-router";
import Code from "~/components/code";
import CodeBlock from "~/components/code-block";
import Dialog, { DialogPanel } from "~/components/dialog";
import Input from "~/components/input";
import { Menu, MenuContent, MenuItem, MenuTrigger } from "~/components/menu";
@@ -36,10 +36,8 @@ export default function NewMachine(data: NewMachineProps) {
<Dialog isOpen={pushDialog} onOpenChange={setPushDialog}>
<DialogPanel isDisabled={!form.canSubmit}>
<Title>Register Machine Key</Title>
<Text className="mb-4">
The machine key is given when you run{" "}
<Code isCopyable>tailscale up --login-server={data.server}</Code> on your device.
</Text>
<Text>The machine key is given when you run the following command on your device:</Text>
<CodeBlock className="mb-4">{`tailscale up --login-server=${data.server}`}</CodeBlock>
<input name="action_id" type="hidden" value="register" />
<Input
{...form.field("register_key")}
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react";
import { useFetcher } from "react-router";
import Button from "~/components/button";
import Code from "~/components/code";
import CodeBlock from "~/components/code-block";
import Dialog, { DialogPanel } from "~/components/dialog";
import Input from "~/components/input";
import Link from "~/components/link";
@@ -12,7 +12,6 @@ import Switch from "~/components/switch";
import Text from "~/components/text";
import Title from "~/components/title";
import type { User } from "~/types";
import toast from "~/utils/toast";
import { getUserDisplayName } from "~/utils/user";
interface AddAuthKeyProps {
@@ -95,23 +94,11 @@ export default function AddAuthKey({
<DialogPanel variant="unactionable">
<Title>Pre-auth key created</Title>
<Text>Copy this key now. You will not be able to see the full key again.</Text>
<div className="mt-4 flex items-center gap-2 rounded-lg bg-mist-100 px-3 py-2 dark:bg-mist-800">
<code className="min-w-0 flex-1 truncate font-mono text-sm">{createdKey}</code>
<Button
className="shrink-0"
onClick={async () => {
await navigator.clipboard.writeText(createdKey);
toast("Copied key to clipboard");
}}
variant="light"
>
Copy
</Button>
</div>
<CodeBlock className="mt-4">{createdKey}</CodeBlock>
<Text className="mt-4 text-sm">To register a device with this key:</Text>
<Code isCopyable className="mt-1 block text-sm">
<CodeBlock className="mt-1">
{`tailscale up --login-server=${url} --authkey ${createdKey}`}
</Code>
</CodeBlock>
</DialogPanel>
) : (
<DialogPanel