# 패키지 관리자 탭

npm, pnpm, yarn, bun 탭으로 설치 명령을 보여 줘요. 고른 것을 기억해요.

- Type: block
- Theme: base
- Tags: arc, block, install, tabs, npm, pnpm, code
- English title: Package manager tabs
- URL: https://dsui.vercel.app/items/package-manager-tabs

## Used items

- [탭](https://dsui.vercel.app/items/tabs): 탭을 바꾸면 패널 높이가 부드럽게 이어지며 내용이 바뀌어요.
- [코드 블록](https://dsui.vercel.app/items/code-block): 파일 이름과 함께 코드를 보여 주고 길면 접을 수 있어요.

## Controls

| Key | Type | Label | Default |
| --- | --- | --- | --- |
| filename | text | 파일 이름 | 터미널 |
| label | text | 탭 이름 | 패키지 관리자 |

## Source

### package-manager-tabs.module.css

```css
/* 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.tsx

```tsx
"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;

```
