mirror of
https://github.com/tale/headplane.git
synced 2026-08-24 19:46:29 +00:00
feat: separate code snippets and code blocks for copyable snippets
This commit is contained in:
@@ -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
@@ -1,51 +1,19 @@
|
|||||||
import { Check, Copy } from "lucide-react";
|
|
||||||
import { HTMLProps } from "react";
|
|
||||||
|
|
||||||
import cn from "~/utils/cn";
|
import cn from "~/utils/cn";
|
||||||
import toast from "~/utils/toast";
|
|
||||||
|
|
||||||
export interface CodeProps extends HTMLProps<HTMLSpanElement> {
|
export interface CodeProps {
|
||||||
isCopyable?: boolean;
|
|
||||||
children: string | string[] | number;
|
children: string | string[] | number;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Code({ isCopyable, children, className }: CodeProps) {
|
export default function Code({ children, className }: CodeProps) {
|
||||||
return (
|
return (
|
||||||
<code
|
<code
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-mist-100 dark:bg-mist-800 px-1 py-0.5 font-mono",
|
"bg-mist-100 dark:bg-mist-800 px-1.5 py-0.5 font-mono rounded-sm text-[0.875em]",
|
||||||
"rounded-sm focus-within:outline-hidden focus-within:ring-2",
|
|
||||||
isCopyable && "relative pr-7",
|
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{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>
|
</code>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-25
@@ -1,4 +1,4 @@
|
|||||||
import { Check, Copy } from "lucide-react";
|
import { Check } from "lucide-react";
|
||||||
import { redirect } from "react-router";
|
import { redirect } from "react-router";
|
||||||
|
|
||||||
import androidSvg from "~/assets/android.svg";
|
import androidSvg from "~/assets/android.svg";
|
||||||
@@ -6,14 +6,13 @@ import iosSvg from "~/assets/ios.svg";
|
|||||||
import linuxSvg from "~/assets/linux.svg";
|
import linuxSvg from "~/assets/linux.svg";
|
||||||
import macosSvg from "~/assets/macos.svg";
|
import macosSvg from "~/assets/macos.svg";
|
||||||
import windowsSvg from "~/assets/windows.svg";
|
import windowsSvg from "~/assets/windows.svg";
|
||||||
import Button from "~/components/button";
|
|
||||||
import Card from "~/components/card";
|
import Card from "~/components/card";
|
||||||
|
import CodeBlock from "~/components/code-block";
|
||||||
import Link from "~/components/link";
|
import Link from "~/components/link";
|
||||||
import LinkAccount from "~/layout/link-account";
|
import LinkAccount from "~/layout/link-account";
|
||||||
import { usersResource } from "~/server/headscale/live-store";
|
import { usersResource } from "~/server/headscale/live-store";
|
||||||
import { Capabilities } from "~/server/web/roles";
|
import { Capabilities } from "~/server/web/roles";
|
||||||
import cn from "~/utils/cn";
|
import cn from "~/utils/cn";
|
||||||
import toast from "~/utils/toast";
|
|
||||||
import { getUserDisplayName } from "~/utils/user";
|
import { getUserDisplayName } from "~/utils/user";
|
||||||
|
|
||||||
import type { Route } from "./+types/home";
|
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} />
|
<img alt="Linux" className="w-4 dark:invert" src={linuxSvg} />
|
||||||
<span className="text-sm font-medium">Linux</span>
|
<span className="text-sm font-medium">Linux</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 flex items-center gap-2">
|
<CodeBlock className="mt-2">curl -fsSL https://tailscale.com/install.sh | sh</CodeBlock>
|
||||||
<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>
|
|
||||||
<p className="mt-1 text-xs text-mist-500 dark:text-mist-400">
|
<p className="mt-1 text-xs text-mist-500 dark:text-mist-400">
|
||||||
<Link
|
<Link
|
||||||
external
|
external
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Computer, FileKey2 } from "lucide-react";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
|
|
||||||
import Code from "~/components/code";
|
import CodeBlock from "~/components/code-block";
|
||||||
import Dialog, { DialogPanel } from "~/components/dialog";
|
import Dialog, { DialogPanel } from "~/components/dialog";
|
||||||
import Input from "~/components/input";
|
import Input from "~/components/input";
|
||||||
import { Menu, MenuContent, MenuItem, MenuTrigger } from "~/components/menu";
|
import { Menu, MenuContent, MenuItem, MenuTrigger } from "~/components/menu";
|
||||||
@@ -36,10 +36,8 @@ export default function NewMachine(data: NewMachineProps) {
|
|||||||
<Dialog isOpen={pushDialog} onOpenChange={setPushDialog}>
|
<Dialog isOpen={pushDialog} onOpenChange={setPushDialog}>
|
||||||
<DialogPanel isDisabled={!form.canSubmit}>
|
<DialogPanel isDisabled={!form.canSubmit}>
|
||||||
<Title>Register Machine Key</Title>
|
<Title>Register Machine Key</Title>
|
||||||
<Text className="mb-4">
|
<Text>The machine key is given when you run the following command on your device:</Text>
|
||||||
The machine key is given when you run{" "}
|
<CodeBlock className="mb-4">{`tailscale up --login-server=${data.server}`}</CodeBlock>
|
||||||
<Code isCopyable>tailscale up --login-server={data.server}</Code> on your device.
|
|
||||||
</Text>
|
|
||||||
<input name="action_id" type="hidden" value="register" />
|
<input name="action_id" type="hidden" value="register" />
|
||||||
<Input
|
<Input
|
||||||
{...form.field("register_key")}
|
{...form.field("register_key")}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react";
|
|||||||
import { useFetcher } from "react-router";
|
import { useFetcher } from "react-router";
|
||||||
|
|
||||||
import Button from "~/components/button";
|
import Button from "~/components/button";
|
||||||
import Code from "~/components/code";
|
import CodeBlock from "~/components/code-block";
|
||||||
import Dialog, { DialogPanel } from "~/components/dialog";
|
import Dialog, { DialogPanel } from "~/components/dialog";
|
||||||
import Input from "~/components/input";
|
import Input from "~/components/input";
|
||||||
import Link from "~/components/link";
|
import Link from "~/components/link";
|
||||||
@@ -12,7 +12,6 @@ import Switch from "~/components/switch";
|
|||||||
import Text from "~/components/text";
|
import Text from "~/components/text";
|
||||||
import Title from "~/components/title";
|
import Title from "~/components/title";
|
||||||
import type { User } from "~/types";
|
import type { User } from "~/types";
|
||||||
import toast from "~/utils/toast";
|
|
||||||
import { getUserDisplayName } from "~/utils/user";
|
import { getUserDisplayName } from "~/utils/user";
|
||||||
|
|
||||||
interface AddAuthKeyProps {
|
interface AddAuthKeyProps {
|
||||||
@@ -95,23 +94,11 @@ export default function AddAuthKey({
|
|||||||
<DialogPanel variant="unactionable">
|
<DialogPanel variant="unactionable">
|
||||||
<Title>Pre-auth key created</Title>
|
<Title>Pre-auth key created</Title>
|
||||||
<Text>Copy this key now. You will not be able to see the full key again.</Text>
|
<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">
|
<CodeBlock className="mt-4">{createdKey}</CodeBlock>
|
||||||
<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>
|
|
||||||
<Text className="mt-4 text-sm">To register a device with this key:</Text>
|
<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}`}
|
{`tailscale up --login-server=${url} --authkey ${createdKey}`}
|
||||||
</Code>
|
</CodeBlock>
|
||||||
</DialogPanel>
|
</DialogPanel>
|
||||||
) : (
|
) : (
|
||||||
<DialogPanel
|
<DialogPanel
|
||||||
|
|||||||
Reference in New Issue
Block a user