Repository Public MIT
1"use client";2import { AlertTriangle, ChevronDown } from "lucide-react";3import { useId, useState } from "react";4import type { InstallInstructions } from "@skills/registry-core";5import { CopyButton } from "./copy-button";67function Command({ command }: { command: string }) {8 return (9 <div className="code-frame">10 <div className="flex items-start gap-3 px-3.5 py-2.5">11 <pre className="min-w-0 flex-1 overflow-x-auto font-mono text-[13px] leading-relaxed text-text" tabIndex={0}>12 <code className="prompt whitespace-pre">{command}</code>13 </pre>14 <CopyButton text={command} />15 </div>16 </div>17 );18}1920/** Client-specific install instructions: the one command first, the manual path one click away. */21export function InstallTabs({ instructions }: { instructions: InstallInstructions[] }) {22 const [active, setActive] = useState(0);23 const id = useId();24 if (instructions.length === 0) return <p className="text-sm text-muted">No client-specific installation is defined for this release. Download the archive and follow its README.</p>;25 const cur = instructions[active]!;26 const [first, ...rest] = cur.steps;27 return (28 <div className="card overflow-hidden">29 <div role="tablist" aria-label="Client" className="flex gap-1 overflow-x-auto border-b border-line bg-raised/50 px-2 pt-2">30 {instructions.map((ins, i) => (31 <button32 key={ins.client}33 role="tab"34 type="button"35 id={`${id}-tab-${i}`}36 aria-selected={i === active}37 aria-controls={`${id}-panel`}38 tabIndex={i === active ? 0 : -1}39 onClick={() => setActive(i)}40 onKeyDown={(e) => {41 if (e.key === "ArrowRight" || e.key === "ArrowLeft") {42 const n = (i + (e.key === "ArrowRight" ? 1 : -1) + instructions.length) % instructions.length;43 setActive(n);44 document.getElementById(`${id}-tab-${n}`)?.focus();45 }46 }}47 className={`-mb-px whitespace-nowrap rounded-t-lg border border-b-0 px-3.5 py-2 text-sm font-medium ${i === active ? "border-line bg-graphite text-text" : "border-transparent text-muted hover:text-text"}`}48 >49 {ins.clientLabel}50 </button>51 ))}52 </div>53 <div role="tabpanel" id={`${id}-panel`} aria-labelledby={`${id}-tab-${active}`} className="space-y-4 p-4 sm:p-5" tabIndex={0}>54 {first && (55 <div>56 <p className="mb-2 text-sm font-medium text-text">{first.title}</p>57 {first.command && <Command command={first.command} />}58 {first.note && <p className="mt-2 break-words text-xs text-muted">{first.note}</p>}59 </div>60 )}61 {cur.destination && (62 <dl className="grid gap-x-6 gap-y-1 rounded-lg border border-line bg-raised/40 px-3.5 py-2.5 font-mono text-[12px] sm:grid-cols-2">63 {cur.destination.user && (<div><dt className="text-faint">Installs to (user)</dt><dd className="break-all text-text">{cur.destination.user}</dd></div>)}64 {cur.destination.project && (<div><dt className="text-faint">{cur.destination.user ? "With --project" : "Extracts to"}</dt><dd className="break-all text-text">{cur.destination.project}</dd></div>)}65 </dl>66 )}67 {cur.config.map((c) => (68 <div key={c.file} className="code-frame">69 <div className="code-head">70 <span className="truncate font-mono text-[11px] text-faint">{c.file}</span>71 <CopyButton text={c.content} />72 </div>73 <pre className="code-body" tabIndex={0}>74 <code>{c.content}</code>75 </pre>76 </div>77 ))}78 {cur.requiredEnv.length > 0 && (79 <div>80 <p className="eyebrow mb-2">Environment variables — set locally, never sent to the registry</p>81 <div className="overflow-x-auto rounded-lg border border-line">82 <table className="data-table">83 <thead><tr><th scope="col">Variable</th><th scope="col">Required</th><th scope="col">Purpose</th></tr></thead>84 <tbody>85 {cur.requiredEnv.map((e) => (86 <tr key={e.name}>87 <td className="whitespace-nowrap font-mono text-[12px] text-text">{e.name}{e.secret && <span className="ml-2 rounded border border-warn/40 px-1 text-[10px] uppercase text-warn">secret</span>}</td>88 <td className="text-muted">{e.required ? "yes" : "optional"}</td>89 <td className="text-muted">{e.description ?? "—"}</td>90 </tr>91 ))}92 </tbody>93 </table>94 </div>95 </div>96 )}97 {rest.length > 0 && (98 <details className="group rounded-lg border border-line">99 <summary className="flex cursor-pointer list-none items-center justify-between gap-3 px-3.5 py-2.5 text-sm font-medium text-text [&::-webkit-details-marker]:hidden">100 <span>{cur.strategy === "mcp-config" ? "More steps" : `Without the CLI: download, verify, extract (${rest.length} steps)`}</span>101 <ChevronDown size={15} aria-hidden className="text-faint transition-transform group-open:rotate-180" />102 </summary>103 <ol className="space-y-3 border-t border-line p-3.5">104 {rest.map((s, i) => (105 <li key={i}>106 <p className="mb-1.5 text-sm text-text"><span className="mr-2 inline-grid h-5 w-5 place-items-center rounded-full border border-line-strong font-mono text-[10px] text-faint">{i + 1}</span>{s.title}</p>107 {s.command && <Command command={s.command} />}108 {s.note && <p className="mt-1.5 break-words text-xs text-muted">{s.note}</p>}109 </li>110 ))}111 </ol>112 </details>113 )}114 {cur.warnings.length > 0 && (115 <div className="callout border-warn/40 bg-warn/5">116 <AlertTriangle size={15} aria-hidden className="mt-0.5 shrink-0 text-warn" />117 <ul className="space-y-1 text-[13px] text-muted">118 {cur.warnings.map((w) => (<li key={w}>{w}</li>))}119 </ul>120 </div>121 )}122 </div>123 </div>124 );125}126