npm package: @seojaeho/app-launcher
Description: Unified app launcher menu component for CROSS ecosystem services
Latest version: 0.1.11

# @seojaeho/app-launcher

CROSS 생태계 서비스 전체에서 사용하는 통합 앱 런처 메뉴 컴포넌트.

S3에 호스팅된 글로벌 메뉴 메타데이터를 fetch하여 일관된 UI로 렌더링한다. Trigger는 headless(소비자가 자유롭게 구성), Content는 패키지가 디자인을 통제한다. Desktop에서는 Popover, Mobile에서는 Drawer로 자동 전환된다.

## Install

```bash
npm install @seojaeho/app-launcher
```

### Peer Dependencies

소비자 프로젝트에 아래 패키지가 이미 설치되어 있어야 한다.

- `react` >= 18
- `react-dom` >= 18
- `@tanstack/react-query` >= 5

## Quick Start

```tsx
import {
  AppLauncher,
  AppLauncherTrigger,
  AppLauncherContent,
} from "@seojaeho/app-launcher";

function MyNav() {
  return (
    <AppLauncher env="production" theme="dark">
      <AppLauncherTrigger asChild>
        <button>Menu</button>
      </AppLauncherTrigger>
      <AppLauncherContent />
    </AppLauncher>
  );
}
```

별도의 CSS import 없이 스타일이 자동 적용된다.

## Components

### `<AppLauncher>`

Root 컴포넌트. Context Provider + Popover/Drawer Root 역할.

| Prop               | Type                               | Default    | Description                                    |
| ------------------ | ---------------------------------- | ---------- | ---------------------------------------------- |
| `env`              | `'dev' \| 'stage' \| 'production'` | **(필수)** | 환경에 따라 메뉴 아이템의 URL이 결정된다       |
| `theme`            | `'dark' \| 'light'`                | `'dark'`   | 팝오버/드로어의 다크/라이트 테마                |
| `mobileBreakpoint` | `number`                           | `768`      | 이 px 이하에서 Drawer로 전환 (반응형 기준점)   |

### `<AppLauncherTrigger>`

Headless trigger. 소비자가 제공하는 엘리먼트를 trigger로 사용한다.

| Prop      | Type      | Default | Description                                            |
| --------- | --------- | ------- | ------------------------------------------------------ |
| `asChild` | `boolean` | `true`  | `true`이면 자식 엘리먼트를 trigger로 사용 (Radix 패턴) |

### `<AppLauncherContent>`

Styled content. 패키지가 디자인을 통제한다. 2열 그리드 레이아웃.

| Prop         | Type                           | Default | Description                     |
| ------------ | ------------------------------ | ------- | ------------------------------- |
| `align`      | `'start' \| 'center' \| 'end'` | `'end'` | 팝오버 정렬 위치 (Desktop only) |
| `sideOffset` | `number`                       | `12`    | trigger와의 간격 px (Desktop)   |
| `className`  | `string`                       | -       | 추가 CSS 클래스                 |

## Responsive

- **Desktop** (`> mobileBreakpoint`): Popover로 렌더링
- **Mobile** (`<= mobileBreakpoint`): 하단 Drawer(Bottom Sheet)로 렌더링

소비자는 별도의 분기 처리 없이 패키지가 자동으로 전환한다.

```tsx
<AppLauncher env="production" mobileBreakpoint={1024}>
  {/* 1024px 이하에서 Drawer로 전환 */}
</AppLauncher>
```

## Usage Example

```tsx
import {
  AppLauncher,
  AppLauncherTrigger,
  AppLauncherContent,
} from "@seojaeho/app-launcher";

function NavBar() {
  return (
    <AppLauncher env="production" theme="dark">
      <AppLauncherTrigger asChild>
        <button>
          <DotsGridIcon />
        </button>
      </AppLauncherTrigger>
      <AppLauncherContent align="end" sideOffset={8} />
    </AppLauncher>
  );
}
```

## Data Source

메뉴 데이터는 S3에서 자동으로 fetch된다.

```
https://contents.crosstoken.io/global-menu/global-menu.json
```

- `@tanstack/react-query`의 `useQuery` 사용
- 캐시 전략: `no-cache` (항상 최신 데이터)
- 메뉴 아이템은 `order` 필드 기준으로 정렬

### `useGlobalMenu` Hook

패키지에서 `useGlobalMenu` hook도 export한다. 메뉴 데이터를 직접 사용해야 할 때:

```tsx
import { useGlobalMenu } from "@seojaeho/app-launcher";

function MyComponent() {
  const { data, isLoading } = useGlobalMenu();
  // data.items: GlobalMenuItem[]
}
```

## Styling

- Vanilla CSS + CSS Custom Properties 사용
- 클래스명은 `al-` 접두사로 네임스페이스 충돌 방지
- `injectStyle`로 CSS가 JS에 포함되어 별도 CSS import 불필요
- 테마는 `data-theme` 속성으로 전환

### CSS Custom Properties Override

필요 시 CSS 변수를 override하여 커스텀 가능:

```css
[data-theme="dark"] {
  --al-bg: #222;
  --al-bg-hover: #333;
}
```

| Variable             | Description      |
| -------------------- | ---------------- |
| `--al-bg`            | 배경색           |
| `--al-bg-hover`      | hover 배경색     |
| `--al-text`          | 기본 텍스트 색   |
| `--al-text-secondary`| 보조 텍스트 색   |
| `--al-border`        | 테두리 색        |
| `--al-shadow`        | 박스 그림자      |
| `--al-badge-bg`      | 뱃지 배경색      |
| `--al-badge-text`    | 뱃지 텍스트 색   |
| `--al-badge-new-bg`  | NEW 뱃지 배경색  |
| `--al-badge-new-text`| NEW 뱃지 텍스트 색|
| `--al-drawer-overlay`| Drawer 오버레이 색|

## Types

```ts
type Environment = "dev" | "stage" | "production";

interface GlobalMenuItem {
  id: string;
  label: string;
  description: string;
  url: { dev: string; stage: string; production: string };
  iconUrl: string;
  order: number;
  type: string;
  badge: string | null;
  isNew: boolean;
}

interface GlobalMenu {
  version: string;
  items: GlobalMenuItem[];
}
```

## Architecture

```
AppLauncher (Provider + Popover.Root / Drawer.Root)
├── AppLauncherTrigger (headless - 소비자 제공)
└── AppLauncherContent (styled - 패키지 통제)
    ├── Desktop → Popover.Content (2열 그리드)
    └── Mobile  → Drawer.Content  (2열 그리드)
```

내부적으로 `useGlobalMenu` hook이 S3에서 메뉴 데이터를 fetch하고, `useMediaQuery`로 반응형을 감지하여 Popover/Drawer를 자동 전환한다.