Block
Package manager tabs
One install command per package manager, remembering the reader's choice.
- #arc
- #block
- #install
- #tabs
- #npm
- #pnpm
- #code
1280px
Install
One command installs the files and packages this item needs.
TerminalTerminalTerminalbash
npx shadcn@latest add https://dsui.vercel.app/r/package-manager-tabs.jsonFiles
components/arc/blocks/package-manager-tabs/package-manager-tabs.module.csscomponents/arc/blocks/package-manager-tabs/package-manager-tabs.tsx
Registry items it pulls in
Source
View as Markdownpackage-manager-tabs.module.csspackage-manager-tabs.module.csspackage-manager-tabs.module.csscss
/* The tab row and the block share one outline, so the command reads as one thing rather than a control above a box. */
.tabs { display: flex; flex-direction: column; gap: var(--space-3); }
.tabs pre { margin: 0; }
package-manager-tabs.tsxpackage-manager-tabs.tsxpackage-manager-tabs.tsxtsx
"use client";
import { useSyncExternalStore } from "react";
import { CodeBlock } from "../../code-block/code-block";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../tabs/tabs";
import styles from "./package-manager-tabs.module.css";
export const PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"] as const;
export type PackageManager = (typeof PACKAGE_MANAGERS)[number];
/* One choice for the whole page, kept outside React so every block on the page moves together — a reader who picks
pnpm once should not have to pick it again further down. It is remembered in this browser and nowhere else. */
const listeners = new Set<() => void>();
let chosen: string | null = null;
function read(managers: readonly string[], storageKey: string): string {
if (chosen) return chosen;
try {
const stored = window.localStorage.getItem(storageKey);
if (stored && managers.includes(stored)) chosen = stored;
} catch {
// Storage can be blocked (private mode, sandboxed previews): fall back to the first manager.
}
return chosen ?? managers[0];
}
function choose(value: string, storageKey: string) {
chosen = value;
try {
window.localStorage.setItem(storageKey, value);
} catch {
// Not remembered, still switched for this page.
}
for (const listener of listeners) listener();
}
function subscribe(listener: () => void) {
listeners.add(listener);
return () => { listeners.delete(listener); };
}
export interface PackageManagerTabsProps {
/** The command per manager, keyed by its name. */
commands: Record<string, string>;
/** Which managers to show, in order. Default: npm, pnpm, yarn, bun. */
managers?: readonly string[];
/** Where the choice is remembered in this browser. */
storageKey?: string;
/** The accessible name of the tab row. */
label?: string;
/** The name shown on the block's header. */
filename?: string;
className?: string;
}
/**
* One install command, shown for each package manager. Choosing a manager switches every block on the page and this
* browser remembers it; nothing is stored on the server.
*/
export function PackageManagerTabs({
commands,
managers = PACKAGE_MANAGERS,
storageKey = "package-manager",
label = "패키지 관리자",
filename = "터미널",
className,
}: PackageManagerTabsProps) {
const active = useSyncExternalStore(subscribe, () => read(managers, storageKey), () => managers[0]);
return (
<Tabs value={active} onValueChange={value => choose(value, storageKey)} className={[styles.tabs, className].filter(Boolean).join(" ")}>
<TabsList aria-label={label}>
{managers.map(pm => <TabsTrigger key={pm} value={pm}>{pm}</TabsTrigger>)}
</TabsList>
{managers.map(pm => (
<TabsContent key={pm} value={pm}>
<CodeBlock code={commands[pm] ?? ""} language="bash" filename={filename} />
</TabsContent>
))}
</Tabs>
);
}
PackageManagerTabs.displayName = "PackageManagerTabs";
export default PackageManagerTabs;
Used items
- TabsComponent
Tabs that switch between panels, morphing the panel height and direction of travel.
- Code blockComponent
A code snippet with an optional filename and line-limited collapse.