feat: refine installer tracking and code block

This commit is contained in:
ibelick
2026-02-12 08:51:47 +01:00
parent 7e2d820a85
commit f2d09e3b2c
2 changed files with 122 additions and 29 deletions
+2
View File
@@ -29,6 +29,8 @@ function track_install() {
curl -sS -X POST \
-H "Content-Type: application/json" \
-H "Origin: https://webclaw.dev" \
-H "Referer: https://webclaw.dev/" \
-d "{\"name\":\"install\",\"event\":\"install\",\"properties\":{\"source\":\"install-script\",\"os\":\"${os}\",\"arch\":\"${arch}\"}}" \
"$TRACKING_URL" >/dev/null 2>&1 || true
}
+120 -29
View File
@@ -1,4 +1,5 @@
import type { ReactNode } from "react";
import { useState } from "react";
type CodeBlockProps = {
className?: string;
@@ -14,7 +15,7 @@ export function CodeBlock({ className, children }: CodeBlockProps) {
return (
<div className={className}>
<div>
<pre className="m-0 overflow-x-auto rounded-[6px] bg-white p-4 text-[13px]">
<pre className="group relative m-0 overflow-x-auto rounded-[6px] bg-white p-4 text-[13px] text-neutral-800">
{children}
</pre>
</div>
@@ -24,40 +25,130 @@ export function CodeBlock({ className, children }: CodeBlockProps) {
export function CodeBlockCode({ code, language }: CodeBlockCodeProps) {
const resolvedLanguage = language ?? "text";
const [hasCopied, setHasCopied] = useState(false);
const lines = code.split("\n");
if (resolvedLanguage === "bash") {
return (
<code className="whitespace-pre" data-language={resolvedLanguage}>
{lines.map((line, index) => {
const match = /^(gh|cd|pnpm)\s(.+)$/.exec(line);
const command = match?.[1];
const rest = match ? ` ${match[2]}` : line;
async function handleCopy() {
if (!navigator.clipboard?.writeText) {
return;
}
return (
<span key={`${line}-${index}`}>
{command === "gh" ? (
<span style={{ color: "rgb(111, 66, 193)" }}>gh</span>
) : command === "cd" ? (
<span style={{ color: "#005CC5" }}>cd</span>
) : command === "pnpm" ? (
<span style={{ color: "rgb(111, 66, 193)" }}>pnpm</span>
) : null}
<span style={{ color: "#032F62" }}>{rest}</span>
{index < lines.length - 1 ? "\n" : ""}
</span>
);
})}
</code>
try {
await navigator.clipboard.writeText(code);
setHasCopied(true);
setTimeout(() => setHasCopied(false), 1600);
} catch {
setHasCopied(false);
}
}
function renderBashLine(line: string) {
const match = /^(gh|cd|pnpm|curl)\s(.+)$/.exec(line);
const command = match?.[1];
const rest = match ? ` ${match[2]}` : line;
if (command === "curl") {
const pipeIndex = rest.indexOf("|");
const beforePipe =
pipeIndex === -1 ? rest : rest.slice(0, pipeIndex).trimEnd();
const afterPipe =
pipeIndex === -1 ? "" : rest.slice(pipeIndex + 1).trim();
const urlMatch = /https?:\/\/\S+/.exec(beforePipe);
const segments: ReactNode[] = [];
if (urlMatch) {
const urlStart = urlMatch.index ?? 0;
const urlEnd = urlStart + urlMatch[0].length;
const prefix = beforePipe.slice(0, urlStart);
const url = urlMatch[0];
const suffix = beforePipe.slice(urlEnd);
if (prefix) {
segments.push(<span className="text-neutral-800">{prefix}</span>);
}
segments.push(<span className="text-neutral-500">{url}</span>);
if (suffix) {
segments.push(<span className="text-neutral-800">{suffix}</span>);
}
} else {
segments.push(<span className="text-neutral-800">{beforePipe}</span>);
}
if (afterPipe) {
segments.push(<span className="text-neutral-400">{" | "}</span>);
segments.push(<span className="text-neutral-500">{afterPipe}</span>);
}
return (
<span>
<span className="text-neutral-900">curl</span>
{segments}
</span>
);
}
return (
<span>
{command ? <span className="text-neutral-900">{command}</span> : null}
<span className="text-neutral-500">{rest}</span>
</span>
);
}
return (
<code
className="whitespace-pre text-neutral-900"
data-language={resolvedLanguage}
>
{code}
</code>
<>
<button
type="button"
aria-label={hasCopied ? "Copied" : "Copy command"}
className="absolute right-3 top-1/2 flex size-8 -translate-y-1/2 items-center justify-center rounded-[6px] text-neutral-500 opacity-0 transition duration-150 ease-out group-hover:opacity-100 hover:text-neutral-900"
onClick={handleCopy}
>
{hasCopied ? (
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M5 14L8.5 17.5L19 6.5" />
</svg>
) : (
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M9 15C9 12.1716 9 10.7574 9.87868 9.87868C10.7574 9 12.1716 9 15 9L16 9C18.8284 9 20.2426 9 21.1213 9.87868C22 10.7574 22 12.1716 22 15V16C22 18.8284 22 20.2426 21.1213 21.1213C20.2426 22 18.8284 22 16 22H15C12.1716 22 10.7574 22 9.87868 21.1213C9 20.2426 9 18.8284 9 16L9 15Z" />
<path d="M16.9999 9C16.9975 6.04291 16.9528 4.51121 16.092 3.46243C15.9258 3.25989 15.7401 3.07418 15.5376 2.90796C14.4312 2 12.7875 2 9.5 2C6.21252 2 4.56878 2 3.46243 2.90796C3.25989 3.07417 3.07418 3.25989 2.90796 3.46243C2 4.56878 2 6.21252 2 9.5C2 12.7875 2 14.4312 2.90796 15.5376C3.07417 15.7401 3.25989 15.9258 3.46243 16.092C4.51121 16.9528 6.04291 16.9975 9 16.9999" />
</svg>
)}
</button>
{resolvedLanguage === "bash" ? (
<code className="whitespace-pre" data-language={resolvedLanguage}>
{lines.map((line, index) => (
<span key={`${line}-${index}`}>
{renderBashLine(line)}
{index < lines.length - 1 ? "\n" : ""}
</span>
))}
</code>
) : (
<code
className="whitespace-pre text-neutral-800"
data-language={resolvedLanguage}
>
{code}
</code>
)}
</>
);
}