SPB Git

Repository Public MIT

install-tabs.tsx 6.2 KB 126 lines tsx

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
Files main
Menu

Theme