pm-tabs.tsx72 lines · main
1'use client';
2
3import { useEffect, useState } from 'react';
4
5import { PMS, type Pm, type PmBlock } from '../lib/pm';
6
7const STORAGE_KEY = 'briven.docs.pm';
8const PM_CHANGE_EVENT = 'briven:pm-change';
9
10/**
11 * Tabbed code block that shows the same command expressed in each supported
12 * package manager (npm / pnpm / yarn / bun). The user's choice is persisted
13 * in localStorage and broadcast to every other PmTabs on the page so the
14 * whole doc switches together.
15 */
16export function PmTabs({ commands }: { commands: PmBlock }) {
17 const [pm, setPm] = useState<Pm>('npm');
18
19 useEffect(() => {
20 const saved = window.localStorage.getItem(STORAGE_KEY);
21 if (isPm(saved)) setPm(saved);
22 const onChange = (e: Event) => {
23 const next = (e as CustomEvent<Pm>).detail;
24 if (isPm(next)) setPm(next);
25 };
26 window.addEventListener(PM_CHANGE_EVENT, onChange);
27 return () => window.removeEventListener(PM_CHANGE_EVENT, onChange);
28 }, []);
29
30 function select(next: Pm) {
31 setPm(next);
32 window.localStorage.setItem(STORAGE_KEY, next);
33 window.dispatchEvent(new CustomEvent<Pm>(PM_CHANGE_EVENT, { detail: next }));
34 }
35
36 return (
37 <div className="mt-2 overflow-hidden rounded-md border border-[var(--color-border)] bg-[var(--color-surface)]">
38 <div
39 role="tablist"
40 aria-label="package manager"
41 className="flex border-b border-[var(--color-border)]"
42 >
43 {PMS.map((p) => {
44 const active = p === pm;
45 return (
46 <button
47 key={p}
48 type="button"
49 role="tab"
50 aria-selected={active}
51 onClick={() => select(p)}
52 className={`cursor-pointer px-3 py-1.5 font-mono text-xs transition-colors ${
53 active
54 ? 'bg-[var(--color-surface-raised)] text-[var(--color-text)]'
55 : 'text-[var(--color-text-muted)] hover:text-[var(--color-text)]'
56 }`}
57 >
58 {p}
59 </button>
60 );
61 })}
62 </div>
63 <pre className="overflow-x-auto p-4 font-mono text-xs text-[var(--color-text)]">
64 {commands[pm]}
65 </pre>
66 </div>
67 );
68}
69
70function isPm(v: unknown): v is Pm {
71 return v === 'npm' || v === 'pnpm' || v === 'yarn' || v === 'bun';
72}