# @nexus-cross/dapp-ui

CROSS 생태계 DApp 공통 UI 컴포넌트 패키지.

공통 DApp 기능을 제공한다. 주요 진입점:

- **AppLauncher**: 글로벌 서비스 메뉴
- **WalletInfo**: 지갑 잔액·가격·등락률 조회 UI
- **WalletConnectModal**: 지갑 연결 모달/드로어 UI
- **WalletPortfolio**: 리워드·DEX 포지션 포트폴리오 UI
- **GetOneUsd**: Swap·Bridge·Relay를 묶은 ONEUSD 획득 플로팅/버튼 위젯

> 이전 패키지 이름: `cross-app-launcher` (`@nexus-cross/dapp-ui@0.0.1+` 부터 rename).

## Install

```bash
npm install @nexus-cross/dapp-ui
```

### Peer Dependencies

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

- `react` >= 18
- `react-dom` >= 18
- `@tanstack/react-query` >= 5
- `@radix-ui/react-popover` >= 1
- `vaul` >= 1

## Import

모든 기능은 루트 경로에서 import한다.

```tsx
import {
  AppLauncher,
  AppLauncherTrigger,
  AppLauncherContent,
  WalletInfo,
  WalletConnectModal,
  WalletPortfolio,
  GetOneUsd,
} from "@nexus-cross/dapp-ui";
```

## AI-assisted integration

Give an AI coding agent [`AI_INTEGRATION.md`](./AI_INTEGRATION.md) as the
implementation contract. It contains the canonical Get ONEUSD mount location,
route-persistent floating transaction status rule, mode behavior, and the
boundary between `CrossGetOneUsd` and the low-level `GetOneUsd` ports.

---

## AppLauncher

S3에 호스팅된 글로벌 메뉴 메타데이터를 fetch하여 일관된 UI로 렌더링한다. Desktop에서는 Popover, Mobile에서는 오른쪽 Drawer로 자동 전환된다.

### Quick Start

```tsx
import {
  AppLauncher,
  AppLauncherTrigger,
  AppLauncherContent,
} from "@nexus-cross/dapp-ui";

function MyNav() {
  return (
    <AppLauncher env="stage">
      <AppLauncherTrigger />
      <AppLauncherContent />
    </AppLauncher>
  );
}
```

모든 prop은 optional이며, 별도의 CSS import 없이 스타일이 자동 적용된다.

### `<AppLauncher>`

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

| Prop               | Type                               | Default                    | Description                              |
| ------------------ | ---------------------------------- | -------------------------- | ---------------------------------------- |
| `env`              | `'dev' \| 'stage' \| 'production'` | `'production'`             | 환경에 따라 메뉴 아이템의 URL이 결정된다 |
| `theme`            | `'dark' \| 'light'`                | `'dark'`                   | 다크/라이트 테마                         |
| `mobileBreakpoint` | `number`                           | `768`                      | 이 px 이하에서 Drawer로 전환             |
| `domain`           | `string`                           | `window.location.hostname` | 내부/외부 링크 판별 기준 도메인          |

### `<AppLauncherTrigger>`

Trigger 버튼. children 없이 사용하면 기본 dots grid 아이콘 버튼이 렌더링된다.

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

### `<AppLauncherContent>`

메뉴 콘텐츠. 2열 그리드 레이아웃. 패키지가 디자인을 통제한다.

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

### Link Handling

- **같은 도메인** → `window.location.href`로 이동 (풀 URL 사용)
- **다른 도메인** → `window.open`으로 새 창 열기
- **현재 페이지** → 클릭 무시 (Active State)

### Hooks

```tsx
import { useGlobalMenu } from "@nexus-cross/dapp-ui";

const { data, isLoading } = useGlobalMenu();
```

---

## WalletInfo

지갑 주소를 받아 CROSS·BSC 두 체인의 토큰 잔액, 가격, 24시간 등락률을 조회하고 표시하는 Compound Component. Desktop에서는 Popover, Mobile에서는 Drawer로 자동 전환된다.

Dot notation 패턴(`WalletInfo.Trigger`, `WalletInfo.Content` 등)으로 사용한다.

### Quick Start

```tsx
import { WalletInfo } from "@nexus-cross/dapp-ui";

function MyWallet() {
  return (
    <WalletInfo
      env="stage"
      theme="dark"
      walletAddress="0xe0c8d120...c279"
      onCopyAddress={(address, success) => {
        if (success) toast.success("주소가 복사되었습니다");
      }}
    >
      <WalletInfo.Trigger />
      <WalletInfo.Content />

      {/* 선택: dapp별 네비게이션 메뉴 */}
      <WalletInfo.Nav position="top">
        <nav>
          <a href="/about">About</a>
          <a href="/faq">FAQ</a>
        </nav>
      </WalletInfo.Nav>

      {/* 선택: dapp별 푸터 (Disconnect 등) */}
      <WalletInfo.Footer>
        <button onClick={() => disconnect()}>Disconnect</button>
      </WalletInfo.Footer>
    </WalletInfo>
  );
}
```

### `<WalletInfo>` (Root)

Context Provider + Popover/Drawer Root.

| Prop               | Type                                          | Default        | Description                                         |
| ------------------ | --------------------------------------------- | -------------- | --------------------------------------------------- |
| `env`              | `'dev' \| 'stage' \| 'production'`            | `'production'` | API URL 및 chainId 결정                             |
| `theme`            | `'dark' \| 'light'`                           | `'dark'`       | 다크/라이트 테마                                    |
| `walletAddress`    | `string`                                      | (필수)         | 토큰 잔액을 조회할 지갑 주소                        |
| `mobileBreakpoint` | `number`                                      | `768`          | 이 px 이하에서 Drawer로 전환                        |
| `drawerDirection`  | `'right' \| 'bottom' \| 'left'`               | `'right'`      | 모바일 Drawer 방향                                  |
| `modal`            | `boolean`                                     | `false`        | `true`이면 배경 overlay 표시 (Drawer, Popover 공통) |
| `showBalance`      | `boolean`                                     | `false`        | 토큰 잔고 리스트 표시 여부                          |
| `showForgeToken`   | `boolean`                                     | `false`        | Forge(본딩커브) 토큰 보유 리스트 추가 표시 여부     |
| `showGameToken`    | `boolean`                                     | `true`         | game 카테고리 토큰 표시 여부 (`false`이면 숨김)     |
| `showQR`           | `boolean`                                     | `true`         | QR 코드 버튼/뷰 활성화 여부                        |
| `onCopyAddress`    | `(address: string, success: boolean) => void` | -              | 주소 복사 콜백 (토스트 표시용)                      |
| `onBuy`            | `() => void`                                  | -              | 기본 액션 row의 Buy 클릭 핸들러                     |
| `showOnePop`       | `boolean`                                     | `true`         | 액션 row의 ONEpop 카드 + ONEpop 화면 노출 (아래 참조) |
| `onDisconnect`     | `() => void`                                  | -              | 지정 시 하단에 기본 Disconnect(Log Out) 버튼 렌더   |
| `disconnectLabel`  | `string`                                      | `'Disconnect'` | 기본 Disconnect 버튼 라벨                           |
| `open`             | `boolean`                                     | -              | 외부에서 열림 상태를 제어 (controlled)              |
| `onOpenChange`     | `(open: boolean) => void`                     | -              | 열림 상태 변경 콜백                                 |
| `style`            | `WalletInfoStyle`                             | -              | CSS 커스텀 변수 오버라이드 (자동완성 지원)          |

### `<WalletInfo.Trigger>`

Trigger 버튼. children 없이 사용하면 지갑 아이콘 + 축약 주소가 렌더링된다.

| Prop        | Type      | Default | Description                               |
| ----------- | --------- | ------- | ----------------------------------------- |
| `asChild`   | `boolean` | `true`  | `true`이면 자식 엘리먼트를 trigger로 사용 |
| `className` | `string`  | -       | 추가 CSS 클래스 (배경색 커스터마이징 등)  |

```tsx
// 기본 트리거 (지갑 아이콘 + 축약 주소)
<WalletInfo.Trigger />

// 배경색 커스텀 (Tailwind 예시)
<WalletInfo.Trigger className="bg-emerald-900!" />

// 완전 커스텀 트리거
<WalletInfo.Trigger>
  <button>My Wallet</button>
</WalletInfo.Trigger>
```

### `<WalletInfo.Content>`

지갑 정보 콘텐츠. 헤더, 주소 바, 토큰 리스트(가격·등락률·보유 수량·USD 환산가)를 렌더링한다.

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

### `<WalletInfo.Nav>`

dapp별 네비게이션 메뉴 슬롯. 스크롤 영역 외부에 고정 배치된다.

| Prop       | Type                | Default | Description              |
| ---------- | ------------------- | ------- | ------------------------ |
| `position` | `'top' \| 'bottom'` | `'top'` | 토큰 리스트 위/아래 배치 |
| `children` | `ReactNode`         | (필수)  | 네비게이션 콘텐츠        |

### `<WalletInfo.Footer>`

하단 고정 영역 슬롯. `My Profile`·`Settings` 같은 **임의의 커스텀 버튼**을 배치한다. wallet·QR 뷰에서만 노출되고 portfolio·send 뷰에서는 숨겨진다. 전용 버튼 배열 prop은 없고 children으로 직접 구성한다.

Log Out 처리는 레이아웃 목표에 따라 택일한다: **(A)** 단독 Disconnect만 필요하면 `onDisconnect`(본문 하단·우측 정렬 기본 버튼), **(B)** Profile/Settings/Log Out을 한 덩어리로 묶으려면 `onDisconnect`를 생략하고 세 버튼을 모두 `Footer`에 넣는다. 둘은 위치·정렬이 달라 함께 쓰면 Log Out만 떨어져 보이므로 동시에 쓰지 않는다.

| Prop       | Type        | Default | Description |
| ---------- | ----------- | ------- | ----------- |
| `children` | `ReactNode` | (필수)  | 푸터 콘텐츠 |

### ONEpop 화면

기본 액션 row는 **Buy / Bridge / ONEpop / Portfolio** 이고, Send는 총자산 금액
우측의 아이콘 버튼이다. ONEpop 카드를 누르면 소셜 핸들 드롭 진입 화면이 열린다.

이 화면은 **렌더링만** 한다 — dapp-ui는 `@nexus-cross/pop`(viem 필수)에 의존할
수 없고, one-pop-api `/drops`·`/histories`는 SIWE 서명을 요구하기 때문이다.
표시할 상태와 액션 목적지는 주입받는다.

| Prop                | Type            | Description                                                        |
| ------------------- | --------------- | ------------------------------------------------------------------ |
| `showOnePop`        | `boolean`       | ONEpop 카드/화면 노출 (기본 `true`)                                |
| `onePopSummary`     | `OnePopSummary` | 수령 대기 수·합계, replies 카운트, 최근 활동. 미주입 시 온보딩 화면 |
| `onOnePopOpen`      | `() => void`    | 화면 진입 시 호출 — 조회를 이 시점까지 미루는 트리거                |
| `onOnePopSend`      | `() => void`    | Send POP 카드 + "Send your first POP!"                             |
| `onOnePopClaim`     | `() => void`    | Claim POPs 카드 + 수령 대기 배너                                    |
| `onOnePopActivity`  | `() => void`    | Activity 카드 + "See all activity" + replies 배너                   |
| `onePopLogoSrc`     | `string`        | 브랜드 워드마크 이미지 (미지정 시 내장 SVG)                         |
| `onePopHeroSrc`     | `string`        | 히어로 일러스트 이미지 (미지정 시 내장 SVG)                         |

미주입 콜백의 버튼은 비활성으로 보인다. `Available` 카드는 지갑이 이미 불러온
CROSS 체인 ONEUSD 잔액을 그대로 쓰고, `Get ONEUSD ›`는 `onBuy`(on-ramp)를
재사용한다.

`@nexus-cross/connect-kit-react`를 쓰고 `onePopEnabled: true`면
`onePopSummary`가 자동 주입된다. 자세한 내용은 `docs/dapp-ui/onepop.md`.

```tsx
<WalletInfo
  walletAddress={address}
  showBalance
  onePopSummary={summary}
  onOnePopSend={() => router.push('/onepop/send')}
  onOnePopClaim={() => router.push('/onepop/claim')}
  onOnePopActivity={() => router.push('/onepop/activity')}
>
```

`OnePopSummary`의 금액 필드는 raw wei-scale 문자열(`amountRaw` /
`claimableTotalRaw`) + `decimals` 로 받는다 — 표시 시 소수점 2자리 버림.

### 토큰 노출 정책

| 규칙            | 설명                                                                    |
| --------------- | ----------------------------------------------------------------------- |
| 필터링          | 보유 수량 > 0인 토큰만 표시                                             |
| 정렬            | USD 환산가 내림차순                                                     |
| 고정 노출       | CROSSD(CROSS), CROSS(CROSS), CROSS(BSC)는 잔고 0이어도 항상 최상단 배치 |
| Forge 토큰      | `showForgeToken={true}` 시 Forge 토큰이 기존 리스트 아래에 추가 표시. balance API와 중복되는 토큰은 자동 제거 (balance 우선) |
| 카테고리 필터   | `showGameToken={false}` 시 game 카테고리 토큰 숨김 (pinned 토큰은 영향 없음) |

### 숫자 포맷팅

| 대상         | >= 1         | < 1          | 처리                     |
| ------------ | ------------ | ------------ | ------------------------ |
| 가격         | 소수점 2자리 | 소수점 4자리 | 버림(floor)              |
| 보유 수량    | 소수점 2자리 | 소수점 4자리 | 버림(floor)              |
| USD 환산가   | 소수점 2자리 | 소수점 4자리 | 버림(floor)              |
| 등락률(양수) | -            | -            | 소수점 2자리 버림(floor) |
| 등락률(음수) | -            | -            | 소수점 2자리 올림(ceil)  |
| 등락률(0)    | -            | -            | 부호 없이 `0%` 표시      |

### Hooks

```tsx
import {
  useTokenBalance,
  useTokenStats,
  USER_BALANCE_QUERY_KEY,
  TOKEN_STATS_QUERY_KEY,
} from "@nexus-cross/dapp-ui";

// 토큰 잔액 (CROSS + BSC 두 체인 병렬 fetch)
const { tokens, isLoading } = useTokenBalance("stage", "0xe0c8...");

// 토큰 가격/등락률
const { statsMap } = useTokenStats("stage");

// 트랜잭션 후 잔액 갱신
queryClient.invalidateQueries({ queryKey: [USER_BALANCE_QUERY_KEY] });
```

### API

토큰 잔액과 가격은 CROSS Wallet API에서, Forge 토큰은 Bonding Curve API에서 자동으로 fetch된다.

**Wallet API**

| 엔드포인트                 | 설명                         |
| -------------------------- | ---------------------------- |
| `/v1/public/token/balance` | 토큰 보유 수량 (체인별 호출) |
| `/v1/public/token/stats`   | 토큰 가격·등락률 (전체 조회) |
| `/v1/public/token/info`    | 토큰 카테고리 조회           |

| Environment     | API Base URL                                  | CROSS Chain ID | BSC Chain ID |
| --------------- | --------------------------------------------- | -------------- | ------------ |
| `dev` / `stage` | `https://stg-wallet-server.crosstoken.io/api` | `612044`       | `97`         |
| `production`    | `https://wallet-server.crosstoken.io/api`     | `612055`       | `56`         |

**Forge API** (`showForgeToken={true}` 시 사용)

| 엔드포인트                       | 설명                          |
| -------------------------------- | ----------------------------- |
| `/v1/users/{address}/holdings`   | Forge 토큰 보유 리스트        |
| `/v1/tokens/{address}`           | Forge 토큰 상세 (이미지 등)   |

| Environment     | API Base URL                                       |
| --------------- | -------------------------------------------------- |
| `dev` / `stage` | `https://stg-bonding-curve-api.crosstoken.io/api`  |
| `production`    | `https://bonding-curve-api.crosstoken.io/api`      |

---

## WalletConnectModal

DApp별로 다른 지갑 연결 팝업 UI를 통합하는 Compound Component. Desktop에서는 Dialog, Mobile에서는 Drawer(Bottom Sheet)로 자동 전환된다. 지갑 연결 로직(Wagmi 등)은 외부에서 콜백으로 주입한다.

Dot notation 패턴(`WalletConnectModal.Trigger`, `WalletConnectModal.Content`)으로 사용한다.

### Quick Start

```tsx
import { WalletConnectModal } from "@nexus-cross/dapp-ui";

function ConnectButton() {
  return (
    <WalletConnectModal
      wallets={{
        cross_wallet: () => connect({ connectorType: "cross_wallet" }),
        cross_extension: () => connect({ connectorType: "cross_extension" }),
        metamask: () => connect({ connectorType: "metamask" }),
        binance: () => connect({ connectorType: "binance" }),
      }}
      socialProviders={{
        google: () => signInWithProvider("google"),
        apple: () => signInWithProvider("apple"),
      }}
    >
      <WalletConnectModal.Trigger />
      <WalletConnectModal.Content />
    </WalletConnectModal>
  );
}
```

### `<WalletConnectModal>` (Root)

Context Provider + Dialog/Drawer Root.

| Prop               | Type                                     | Default    | Description                                |
| ------------------ | ---------------------------------------- | ---------- | ------------------------------------------ |
| `wallets`          | `WalletHandlers`                         | (필수)     | 지갑별 연결 콜백 (key가 표시할 지갑 결정)  |
| `socialProviders`  | `SocialHandlers`                         | -          | Google/Apple 소셜 로그인 콜백              |
| `termsUrl`         | `string`                                 | CROSS 공통 약관 | Footer Terms 링크 URL (미설정 시 내부 폴백) |
| `privacyUrl`       | `string`                                 | CROSS 공통 정책 | Footer Privacy 링크 URL (미설정 시 내부 폴백) |
| `theme`            | `'dark' \| 'light'`                      | `'dark'`   | 다크/라이트 테마                           |
| `mobileBreakpoint` | `number`                                 | `768`      | 이 px 이하에서 Drawer로 전환               |
| `drawerDirection`  | `'bottom' \| 'left' \| 'right' \| 'top'` | `'bottom'` | 모바일 Drawer 방향                         |
| `dialogWidth`      | `string`                                 | `'480px'`  | Dialog 너비                                |
| `drawerMaxWidth`   | `string`                                 | -          | Drawer 최대 너비                           |
| `drawerMinWidth`   | `string`                                 | -          | Drawer 최소 너비                           |
| `style`            | `WalletConnectModalStyle`                | -          | CSS 커스텀 변수 오버라이드 (자동완성 지원) |
| `open`             | `boolean`                                | -          | Controlled mode: 외부에서 열림 상태 제어   |
| `onOpenChange`     | `(open: boolean) => void`                | -          | 열림 상태 변경 콜백                        |

#### `WalletHandlers` 타입

```ts
type WalletHandlers = Partial<Record<WalletId, () => void | Promise<void>>>;

// WalletId = 'cross_embedded' | 'cross_wallet' | 'cross_extension' | 'metamask' | 'binance' | 'verse8' | 'tron'
```

객체에 포함된 key의 지갑만 UI에 표시된다. key는 타입 추론이 된다.

`SocialHandlers`는 Google/Apple 소셜 로그인 행에만 사용된다.

```ts
type SocialHandlers = Partial<Record<'google' | 'apple', () => void | Promise<void>>>;
```

### `<WalletConnectModal.Trigger>`

Trigger 버튼. children 없이 사용하면 기본 "Connect Wallet" 버튼이 렌더링된다.

| Prop      | Type      | Default | Description                               |
| --------- | --------- | ------- | ----------------------------------------- |
| `asChild` | `boolean` | auto    | `true`이면 자식 엘리먼트를 trigger로 사용 |

```tsx
// 기본 트리거
<WalletConnectModal.Trigger />

// 커스텀 트리거
<WalletConnectModal.Trigger>
  <button className="my-btn">지갑 연결</button>
</WalletConnectModal.Trigger>
```

### `<WalletConnectModal.Content>`

지갑 선택 콘텐츠. Featured 지갑, 일반 지갑 리스트, 도움말, 이용약관을 렌더링한다.

| Prop        | Type     | Default | Description     |
| ----------- | -------- | ------- | --------------- |
| `className` | `string` | -       | 추가 CSS 클래스 |

### 스타일 커스터마이징

`style` prop으로 CSS 커스텀 변수를 오버라이드할 수 있다. `WalletConnectModalStyle` 타입이 자동완성을 지원한다.

```tsx
<WalletConnectModal
  wallets={walletHandlers}
  style={{
    '--wcm-dialog-width': '500px',
    '--wcm-drawer-max-width': '500px',
    '--wcm-drawer-min-width': '325px',
    '--wcm-surface-bg': 'var(--color-localGray-0)',
    '--wcm-primary': '#7346f3',
  }}
>
```

사용 가능한 CSS 변수:

| 변수                       | 설명                |
| -------------------------- | ------------------- |
| `--wcm-primary`            | 주요 색상           |
| `--wcm-secondary`          | 보조 색상 (배지 등) |
| `--wcm-surface-bg`         | 모달 배경색         |
| `--wcm-surface-default`    | 아이템 배경색       |
| `--wcm-surface-subtle`     | Installed 배지 배경 |
| `--wcm-border-default`     | 모달 테두리         |
| `--wcm-border-subtle`      | 드로어 핸들 색상    |
| `--wcm-texticon-primary`   | 제목 텍스트 색상    |
| `--wcm-texticon-secondary` | 본문 텍스트 색상    |
| `--wcm-texticon-tertiary`  | 보조 텍스트 색상    |
| `--wcm-dialog-width`       | Dialog 너비         |
| `--wcm-drawer-max-width`   | Drawer 최대 너비    |
| `--wcm-drawer-min-width`   | Drawer 최소 너비    |

### EIP-6963 지갑 감지

패키지 내부에서 EIP-6963 프로토콜(`eip6963:announceProvider`)과 CROSS 커스텀 이벤트(`eip6963:crossAnnounceProvider`)를 통해 설치된 지갑을 자동 감지한다.

- 감지된 지갑: "Installed" 배지 표시, 클릭 시 연결 콜백 실행
- 미설치 지갑(`installUrl` 설정 시): "Install" 버튼 표시, 클릭 시 Chrome Web Store로 이동

```tsx
// 감지 훅을 직접 사용할 수도 있다
import { useWalletDetect } from "@nexus-cross/dapp-ui";

const { wallets, isDetected, isLoading } = useWalletDetect();
if (isDetected("io.metamask")) {
  // MetaMask 설치됨
}
```

### WALLET_REGISTRY

내장된 지갑 메타데이터. 아이콘, 이름, 설명, rdns 등이 포함되어 있다.

| WalletId          | 이름               | 비고                          |
| ----------------- | ------------------ | ----------------------------- |
| `cross_embedded`  | CROSSx with Social | Featured, Google/Apple 로그인 |
| `cross_wallet`    | CROSSx             | 앱 승인 방식                  |
| `cross_extension` | CROSSx Extension   | 브라우저 확장, 데스크톱 전용  |
| `metamask`        | MetaMask           | 브라우저 확장                 |
| `binance`         | Binance            | 모바일 앱 / WalletConnect     |
| `verse8`          | Verse8 Wallet      | For Forge 배지                |
| `tron`            | Tron Wallet        | For SHOP 배지                 |

`@nexus-cross/connect-kit-react`를 쓰는 경우에는 직접 `WalletConnectModal`을
조립하지 않아도 된다. `<CrossConnectKitProvider>`가 이 컴포넌트를 기반으로 한
unified connect modal을 자동 렌더링하며, 지갑 필터링은
`walletAllowlist` / `extraWallets` props로 처리한다.

---

## Styling

두 기능 모두 Vanilla CSS + CSS Custom Properties를 사용하며 별도 CSS import가 불필요하다.

| Feature            | 클래스 접두사 | Content Font | Trigger Font        |
| ------------------ | ------------- | ------------ | ------------------- |
| AppLauncher        | `al-`         | Inter        | Inter               |
| WalletInfo         | `wi-`         | Sora         | inherit (dapp 상속) |
| WalletConnectModal | `wcm-`        | inherit      | inherit (dapp 상속) |

배경색 등을 Tailwind로 커스텀할 때는 `!important`가 필요할 수 있다:

```tsx
<WalletInfo.Trigger className="bg-emerald-900!" />
<WalletInfo.Content className="bg-slate-900!" />
```

## Development

```bash
npm install
npm run build       # 빌드
npm run dev         # watch 모드
npm run typecheck   # 타입 체크
```

## Publishing

npm 배포는 GitHub Actions의 **Publish to npm** 워크플로우(`workflow_dispatch`)를 통해 수동으로 실행한다. 로컬에서 `npm publish`를 직접 실행하지 않는다.

### 브랜치 운영 룰

| 브랜치                       | 배포 타입 |
| ---------------------------- | --------- |
| `master`                     | `prod`    |
| `develop`, `feature/*`, 그 외 | `beta`    |

`prod` 배포는 반드시 `master` 브랜치에서만 실행한다. 그 외 브랜치(`develop`, `feature/*` 등)에서는 `beta`로만 배포한다.

### 배포 절차

1. GitHub 저장소 → **Actions** 탭 → 좌측 **Publish to npm** 워크플로우 선택
2. 우측 상단 **Run workflow** 클릭
3. 아래 입력 항목을 채우고 **Run workflow** 실행

| 항목                | 설명                                                                          |
| ------------------- | ----------------------------------------------------------------------------- |
| `Use workflow from` | 배포할 대상 브랜치 (`prod`은 `master`, `beta`는 `develop`/`feature/*` 등)     |
| `배포 타입`         | `beta` 또는 `prod` 중 선택 (브랜치 운영 룰에 맞게 선택)                       |
| `배포할 버전`       | SemVer 형식의 base 버전 (예: `1.0.0`, `v` 접두사 없이)                        |

### 배포 타입별 동작

| 타입   | 입력 컨벤션                                  | 최종 버전               | npm dist-tag | Git 태그            | GitHub Release |
| ------ | -------------------------------------------- | ----------------------- | ------------ | ------------------- | -------------- |
| `prod` | 현재 npm에 배포된 최신 정식 버전 + 1 (수동)   | 입력한 버전 그대로       | `latest`     | `v{version}`        | 정식 릴리스    |
| `beta` | `0.0.0` 고정 입력                             | `0.0.0-beta.N` 자동      | `beta`       | `v0.0.0-beta.N`     | Pre-release    |

- `beta`의 `N`은 GitHub Releases와 npm registry에 존재하는 최신 beta 번호 중 더 큰 값 + 1로 자동 계산되므로, base 버전은 항상 `0.0.0`으로 고정 입력해도 충돌하지 않는다.
- `prod`는 npm 정책상 동일 버전 재배포가 불가능하므로, [npm @nexus-cross/dapp-ui](https://www.npmjs.com/package/@nexus-cross/dapp-ui) 또는 `package.json`의 현재 버전을 확인 후 `patch`/`minor`/`major` 단위로 1 증가시켜 입력한다.
- 실행 흐름: SonarQube 스캔 → 버전 계산 → 빌드 & npm publish → GitHub Release 생성 → Slack 알림.

### 설치 예시

```bash
# 정식 버전 (prod로 배포된 최신)
npm install @nexus-cross/dapp-ui

# 최신 beta
npm install @nexus-cross/dapp-ui@beta

# 특정 beta 버전 고정
npm install @nexus-cross/dapp-ui@0.0.0-beta.12
```

### 주의 사항

- `prod` 배포는 반드시 `master` 브랜치에서, `beta` 배포는 `develop`/`feature/*` 등 그 외 브랜치에서 실행한다.
- 버전은 반드시 SemVer(`x.y.z`) 형식이어야 하며 `v` 접두사를 붙이지 않는다.
- `beta` 배포는 항상 `0.0.0`을 입력하면 된다. (실제 publish 버전은 `0.0.0-beta.N`으로 자동 증가)
- `prod` 배포는 동일 버전 재배포가 불가능(npm 정책)하므로, 현재 정식 버전에서 1을 올려 입력한다.
- 정식 배포 전 `beta`로 먼저 검증한 뒤 `prod`로 올리는 흐름을 권장한다.
- 배포 전 대상 브랜치의 변경 사항과 현재 `package.json`의 버전이 충돌하지 않는지 확인한다.
