npm package: @shomia/ui-sdk-react
Description: React UI components and styles for Shomia
Latest version: 0.1.0

# @shomia/ui-sdk-react

Shomia の VC（Verifiable Credentials）情報を表示するための React UI SDK です。

## インストール

```bash
npm install @shomia/ui-sdk-react
# or
pnpm add @shomia/ui-sdk-react
```

**Peer Dependencies**: `react >= 18`, `react-dom >= 18`

## セットアップ

```tsx
import "@shomia/ui-sdk-react/styles.css";
```

## コンポーネント

- [`VcDetailCard`](#vcdetailcard) — VC 詳細カード
- [`VcHistory`](#vchistory) — 獲得履歴リスト
- [`VcTypeList`](#vctypelist) — セクション見出し＋グリッド
- [`SkillCard`](#skillcard) — スキルカード
- [`TicketCard`](#ticketcard) — チケットカード（取得済み表示用）
- [`BadgeItem`](#badgeitem) — バッジアイテム
- [`AdminBadgeItem`](#adminbadgeitem) — 管理者バッジアイテム
- [`VcIssuedCard`](#vcissuedcard) — 発行済みVC一覧用カード
- [`VcIssuedList`](#vcissuedlist) — 発行済みVC一覧の details/summary リスト

---

# VcDetailCard

VC の詳細情報を表示するカードコンポーネントです。タイプ別のカラーリング、ステータス表示、説明文を含みます。

## インポート

```tsx
import { VcDetailCard } from "@shomia/ui-sdk-react";
import type { VcDetailCardProps, VcDetailView } from "@shomia/ui-sdk-react";
```

## 基本的な使い方

```tsx
<VcDetailCard
  vc={{
    id: "vc-1",
    type: "Skill",
    title: "React 基礎",
    description: "React の基本的な概念とフックの使い方を理解している",
    imageUrl: "/images/react.png",
    acquiredCount: 3,
    maxNum: 5,
  }}
/>
```

## バリエーション

### Skill（スキル）

青色のカード。獲得数を表示します。`maxNum` を指定すると `3 / 5` のように上限付きで表示します。

```tsx
<VcDetailCard
  vc={{
    id: "vc-1",
    type: "Skill",
    title: "React 基礎",
    description: "React の基本的な概念とフックの使い方を理解している",
    acquiredCount: 3,
    maxNum: 5,
  }}
/>
```

### Badge（バッジ）

オレンジ色のカード。獲得数を表示します。

```tsx
<VcDetailCard
  vc={{
    id: "vc-2",
    type: "Badge",
    title: "ハッカソン優勝",
    description: "社内ハッカソンで優勝したことを証明するバッジ",
    acquiredCount: 1,
  }}
/>
```

### Ticket（チケット）

緑色のカード。`expiredAt` から自動で「有効」/「期限切れ」を判定します。

```tsx
<VcDetailCard
  vc={{
    id: "vc-3",
    type: "Ticket",
    title: "カンファレンス参加券",
    description: "2025年度技術カンファレンス",
    imageUrl: "/images/ticket.png",
    expiredAt: "2025-12-31",
  }}
/>
```

### AdminBadge（管理者バッジ）

紫色のカード。管理者が発行するバッジです。

```tsx
<VcDetailCard
  vc={{
    id: "vc-4",
    type: "AdminBadge",
    title: "プロジェクトリーダー認定",
    description: "プロジェクトをリードし、成果を出したことを証明するバッジです",
    acquiredCount: 1,
  }}
/>
```

### タイプ別の表示ルール

| タイプ | バッジ色 | ステータス表示 |
|--------|---------|--------------|
| Skill | 青 | 獲得数（例: `3` or `3 / 5`） |
| Badge | オレンジ | 獲得数 |
| Ticket | 緑 | 有効 / 期限切れ（`expiredAt` から自動判定） |
| AdminBadge | 紫 | 獲得済み |

## Props

| Prop | 型 | 必須 | 説明 |
|------|-----|------|------|
| `vc` | [`VcDetailView`](#vcdetailview) | Yes | 表示するVCデータ |
| `labels` | [`VcDetailCardLabels`](#vcdetailcardlabels) | - | ラベルテキストの上書き |
| `classNames` | [`VcDetailCardClassNames`](#vcdetailcardclassnames) | - | 各パーツへのクラス追加 |
| `formatDate` | `(iso: string) => string` | - | 日付フォーマット関数 |
| `renderBadge` | `(vc: VcDetailView) => ReactNode` | - | タイプバッジの描画を差し替え |
| `renderImage` | `(vc: VcDetailView) => ReactNode` | - | 画像エリアの描画を差し替え |
| `renderTitle` | `(vc: VcDetailView) => ReactNode` | - | タイトルの描画を差し替え |
| `renderStatus` | `(vc: VcDetailView) => ReactNode` | - | ステータス表示の描画を差し替え |
| `renderMeta` | `(vc: VcDetailView) => ReactNode` | - | メタ情報エリア全体の描画を差し替え |
| `defaultImageUrl` | `string` | - | vc.imageUrl がない場合のフォールバック画像URL |
| `className` | `string` | - | ルート要素に追加するクラス |

> `React.HTMLAttributes<HTMLDivElement>` を継承しているため、`id`, `style`, `data-*` 等も指定可能です。

## 型定義

### VcDetailView

```typescript
// Skill / Badge / AdminBadge
{
  id: string;
  type: "Skill" | "Badge" | "AdminBadge";
  title: string;
  description?: string;
  imageUrl?: string | null;
  imageAlt?: string;
  acquiredCount: number;
  maxNum?: number | null;
}

// Ticket
{
  id: string;
  type: "Ticket";
  title: string;
  description?: string;
  imageUrl?: string | null;
  imageAlt?: string;
  expiredAt?: string | null;  // ISO 日時
}
```

### VcDetailCardLabels

| キー | デフォルト値 | 説明 |
|------|------------|------|
| `acquiredCount` | `"獲得数"` | 獲得数ラベル |
| `statusValid` | `"有効"` | 有効ステータス |
| `statusExpired` | `"期限切れ"` | 期限切れステータス |
| `expiresAt` | `"有効期限"` | 有効期限ラベル |
| `typeSkill` | `"スキル"` | タイプバッジ（Skill） |
| `typeBadge` | `"バッジ"` | タイプバッジ（Badge） |
| `typeTicket` | `"チケット"` | タイプバッジ（Ticket） |
| `typeAdminBadge` | `"管理者バッジ"` | タイプバッジ（AdminBadge） |
| `status` | `"状態"` | メタ情報の「状態」ラベル |

### VcDetailCardClassNames

| キー | 対象 |
|------|------|
| `root` | ルート要素 |
| `header` | ヘッダー |
| `badge` | タイプバッジ |
| `image` | 画像エリア |
| `body` | ボディ |
| `title` | タイトル |
| `status` | ステータス表示 |
| `description` | 説明文 |
| `meta` | メタ情報エリア |

## カスタマイズ

### CSS 変数

```css
:root {
  --shomia-vc-detail-card-radius: 12px;
  --shomia-vc-detail-card-padding: 20px;
  --shomia-vc-detail-card-skill-border: #3b82f6;
  --shomia-vc-detail-card-skill-bg: #eff6ff;
}
```

<details>
<summary>全 CSS 変数一覧</summary>

| 変数名 | デフォルト値 |
|--------|------------|
| `--shomia-vc-detail-card-radius` | `8px` |
| `--shomia-vc-detail-card-padding` | `16px` |
| `--shomia-vc-detail-card-title-color` | `#0f172a` |
| `--shomia-vc-detail-card-desc-color` | `#475569` |
| `--shomia-vc-detail-card-meta-label-color` | `#64748b` |
| `--shomia-vc-detail-card-meta-value-color` | `#0f172a` |
| `--shomia-vc-detail-card-image-size` | `64px` |
| `--shomia-vc-detail-card-image-radius` | `12px` |
| `--shomia-vc-detail-card-status-active-color` | `#0f172a` |
| `--shomia-vc-detail-card-status-expired-color` | `#dc2626` |
| `--shomia-vc-detail-card-skill-border` | `#7dd3fc` |
| `--shomia-vc-detail-card-skill-bg` | `#f0f9ff` |
| `--shomia-vc-detail-card-skill-badge-bg` | `#0284c7` |
| `--shomia-vc-detail-card-skill-badge-color` | `#ffffff` |
| `--shomia-vc-detail-card-badge-border` | `#fdba74` |
| `--shomia-vc-detail-card-badge-bg` | `#fff7ed` |
| `--shomia-vc-detail-card-badge-badge-bg` | `#ca8a04` |
| `--shomia-vc-detail-card-badge-badge-color` | `#ffffff` |
| `--shomia-vc-detail-card-ticket-border` | `#86efac` |
| `--shomia-vc-detail-card-ticket-bg` | `#f0fdf4` |
| `--shomia-vc-detail-card-ticket-badge-bg` | `#22c55e` |
| `--shomia-vc-detail-card-ticket-badge-color` | `#ffffff` |
| `--shomia-vc-detail-card-adminbadge-border` | `#d8b4fe` |
| `--shomia-vc-detail-card-adminbadge-bg` | `#faf5ff` |
| `--shomia-vc-detail-card-adminbadge-badge-bg` | `#a855f7` |
| `--shomia-vc-detail-card-adminbadge-badge-color` | `#ffffff` |

</details>

### classNames

```tsx
<VcDetailCard
  vc={vcData}
  classNames={{
    root: "my-custom-card",
    badge: "my-badge",
    title: "my-detail-card-title",
  }}
/>
```

### render props

```tsx
<VcDetailCard
  vc={vcData}
  renderBadge={(vc) => <span className="my-badge">{vc.type}</span>}
  renderStatus={(vc) => <MyStatusBadge type={vc.type} />}
/>
```

### 日付フォーマット

```tsx
import { format } from "date-fns";

<VcDetailCard
  vc={vcData}
  formatDate={(iso) => format(new Date(iso), "yyyy/MM/dd")}
/>
```

### i18n

```tsx
<VcDetailCard
  vc={vcData}
  labels={{
    acquiredCount: "Acquired",
    statusValid: "Active",
    statusExpired: "Expired",
  }}
/>
```

---

# VcHistory

VC の獲得履歴をリスト表示するコンポーネントです。発行者のアバター、獲得日時、有効期限などを表示します。

## インポート

```tsx
import { VcHistory } from "@shomia/ui-sdk-react";
import type { VcHistoryProps, VcHistoryItem } from "@shomia/ui-sdk-react";
```

## 基本的な使い方

```tsx
<VcHistory
  items={[
    {
      id: "item-1",
      issuer: { id: "user-1", name: "山田太郎" },
      holder: { id: "user-2", name: "鈴木花子" },
      createdAt: "2025-06-15T10:30:00Z",
    },
    {
      id: "item-2",
      issuer: { id: "user-3", name: "佐藤次郎", imageUrl: "/avatars/sato.jpg" },
      holder: { id: "user-2", name: "鈴木花子" },
      createdAt: "2025-07-20T14:00:00Z",
      expiredAt: "2025-12-31",
    },
  ]}
/>
```

## 状態表示

### ローディング

スケルトンUIを表示します。

```tsx
<VcHistory items={[]} loading />
```

### エラー

エラーメッセージを表示します。リストは描画されません。

```tsx
<VcHistory items={[]} error="データの取得に失敗しました" />
```

### 空状態

アイテムが空の場合のカスタム表示です。デフォルトは `"履歴はありません"` と表示されます。

```tsx
<VcHistory items={[]} empty={<p>まだ獲得履歴がありません</p>} />
```

## イベント

### onItemClick

アイテムをクリック可能にします。指定すると各アイテムに `role="button"` とフォーカスリングが付与されます。

```tsx
<VcHistory
  items={historyItems}
  onItemClick={(item, index, event) => {
    console.log("Clicked:", item.id);
  }}
/>
```

## Props

| Prop | 型 | 必須 | 説明 |
|------|-----|------|------|
| `items` | [`VcHistoryItem[]`](#vchistoryitem) | Yes | 獲得履歴データの配列 |
| `labels` | [`VcHistoryLabels`](#vchistorylabels) | - | ラベルテキストの上書き |
| `classNames` | [`VcHistoryClassNames`](#vchistoryclassnames) | - | 各パーツへのクラス追加 |
| `formatDate` | `(iso: string) => string` | - | 日付フォーマット関数 |
| `loading` | `boolean` | - | ローディング状態 |
| `error` | `ReactNode` | - | エラー表示 |
| `empty` | `ReactNode` | - | 空状態の表示 |
| `onItemClick` | `(item: VcHistoryItem, index: number) => void` | - | アイテムクリック時のコールバック |
| `renderItem` | `(item: VcHistoryItem, index: number) => ReactNode` | - | アイテム全体の描画を差し替え |
| `renderAvatar` | `(seed: string, name: string, imageUrl?: string \| null) => ReactNode` | - | アバターの描画を差し替え |
| `renderIssuer` | `(issuer: VcIssuerInfo) => ReactNode` | - | 発行者表示の差し替え |
| `renderHolder` | `(holder: VcHolderInfo) => ReactNode` | - | 保持者表示の差し替え |
| `renderDate` | `(iso: string, item: VcHistoryItem) => ReactNode` | - | 日時表示の差し替え |
| `className` | `string` | - | ルート要素に追加するクラス |

> `React.HTMLAttributes<HTMLDivElement>` を継承しているため、`id`, `style`, `data-*` 等も指定可能です。

## 型定義

### VcHistoryItem

```typescript
{
  id: string;
  issuer: VcIssuerInfo;
  holder: VcHolderInfo;
  createdAt: string;           // ISO 日時
  expiredAt?: string | null;   // ISO 日時（任意）
}
```

### VcIssuerInfo / VcHolderInfo

```typescript
{
  id: string;
  name: string;
  imageUrl?: string | null;
}
```

### VcHistoryLabels

| キー | デフォルト値 | 説明 |
|------|------------|------|
| `issuer` | `"発行者"` | 発行者ラベル |
| `holder` | `"保持者"` | 保持者ラベル |
| `acquiredAt` | `"獲得日時"` | 獲得日時ラベル |
| `expiresAt` | `"有効期限"` | 有効期限ラベル |
| `empty` | `"履歴はありません"` | 空状態テキスト |
| `expiredBadge` | `"期限切れ"` | 期限切れバッジ表示 |

### VcHistoryClassNames

| キー | 対象 |
|------|------|
| `root` | ルート要素 |
| `list` | リストコンテナ |
| `item` | 各アイテム |
| `avatar` | アバターエリア |
| `content` | コンテンツエリア |
| `issuer` | 発行者行 |
| `holder` | 保持者行 |
| `date` | 日時行 |
| `meta` | 有効期限行 |
| `divider` | 区切り線 |
| `empty` | 空状態 |
| `loading` | ローディング |
| `error` | エラー |

## カスタマイズ

### CSS 変数

```css
:root {
  --shomia-vc-history-divider-color: #f1f5f9;
  --shomia-vc-history-label-color: #94a3b8;
}
```

<details>
<summary>全 CSS 変数一覧</summary>

| 変数名 | デフォルト値 |
|--------|------------|
| `--shomia-vc-history-label-color` | `#64748b` |
| `--shomia-vc-history-value-color` | `#0f172a` |
| `--shomia-vc-history-divider-color` | `#e2e8f0` |
| `--shomia-vc-history-expired-color` | `#dc2626` |
| `--shomia-vc-history-expired-badge-bg` | `rgba(220,38,38,0.1)` |
| `--shomia-vc-history-empty-color` | `#94a3b8` |
| `--shomia-vc-history-skeleton-bg` | `#e2e8f0` |

</details>

### classNames

```tsx
<VcHistory
  items={historyItems}
  classNames={{
    item: "my-history-item",
    avatar: "my-avatar-wrapper",
    content: "my-content",
  }}
/>
```

### render props

```tsx
// アバターを差し替え
<VcHistory
  items={historyItems}
  renderAvatar={(seed, name, imageUrl) => (
    <MyCustomAvatar src={imageUrl} name={name} />
  )}
/>

// アイテム全体を差し替え
<VcHistory
  items={historyItems}
  renderItem={(item, index) => (
    <div className="my-item">
      <strong>{item.issuer.name}</strong>
      <time>{new Date(item.createdAt).toLocaleDateString()}</time>
    </div>
  )}
/>
```

### 日付フォーマット

```tsx
import { format } from "date-fns";

<VcHistory
  items={historyItems}
  formatDate={(iso) => format(new Date(iso), "yyyy/MM/dd HH:mm")}
/>
```

### i18n

```tsx
<VcHistory
  items={historyItems}
  labels={{
    issuer: "Issued by",
    holder: "Held by",
    acquiredAt: "Acquired at",
    expiresAt: "Expires",
    empty: "No history",
  }}
/>
```

---
# VcTypeList

セクション見出し（タイトル＋総獲得数）とグリッドレイアウトのラッパーです。ヘッダー下線の色・タイトル文字色・グリッドなどは `classNames` で利用側の CSS クラスを指定します。

## インポート

```tsx
import { VcTypeList } from "@shomia/ui-sdk-react";
import type { VcTypeListProps, VcTypeListClassNames, VcTypeListLabels } from "@shomia/ui-sdk-react";
```

## 基本的な使い方

```tsx
<VcTypeList
  title="スキル"
  totalCount={5}
  classNames={{
    header: "my-section-header",
    title: "my-section-title",
    grid: "my-vc-grid",
  }}
>
  {skillCards}
</VcTypeList>
```

## 状態表示

### ローディング

スケルトンUIを表示します。ヘッダーは表示したまま、グリッド部分をスケルトンに差し替えます。

```tsx
<VcTypeList title="スキル" totalCount={0} loading>
  {[]}
</VcTypeList>
```

### エラー

エラー表示を出します。ヘッダーは表示したまま、その下にエラー内容を表示します。

```tsx
<VcTypeList
  title="スキル"
  totalCount={0}
  error="データの取得に失敗しました"
>
  {[]}
</VcTypeList>
```

### 空状態

`children` が空のときに表示します。`empty` を渡すとその内容、未指定時は `labels.empty`（デフォルト: "獲得したVCはありません"）を表示します。

```tsx
<VcTypeList title="スキル" totalCount={0} empty={<p>まだスキルがありません</p>}>
  {[]}
</VcTypeList>
```

## Props

| Prop | 型 | 必須 | 説明 |
|------|-----|------|------|
| `title` | `string` | Yes | セクションタイトル |
| `totalCount` | `number` | Yes | 総獲得数（「総獲得数: N個」に表示） |
| `labels` | [`VcTypeListLabels`](#vctypelistlabels) | - | ラベル文言の上書き（i18n 用） |
| `classNames` | [`VcTypeListClassNames`](#vctypelistclassnames) | - | 各パーツに追加するクラス名 |
| `loading` | `boolean` | - | ローディング状態（スケルトン表示） |
| `error` | `ReactNode` | - | エラー表示 |
| `empty` | `ReactNode` | - | 空状態の表示（未指定時は `labels.empty`） |
| `children` | `ReactNode` | Yes | グリッド内に並べるカード要素 |

## 型定義

### VcTypeListLabels

| キー | デフォルト値 | 説明 |
|------|------------|------|
| `totalLabel` | `"総獲得数"` | 「総獲得数」部分 |
| `totalUnit` | `"個"` | 個数単位 |
| `empty` | `"獲得したVCはありません"` | 空状態のデフォルト文言 |

### VcTypeListClassNames

| キー | 対象 |
|------|------|
| `root` | ルート要素 |
| `header` | ヘッダー（見出し＋総獲得数）。下線の色指定などに利用 |
| `headerInner` | ヘッダー内側の flex コンテナ |
| `title` | タイトル。文字色・フォントサイズなどに利用 |
| `total` | 総獲得数表示 |
| `body` | ボディ（グリッドの外側） |
| `grid` | グリッド。利用側で列数・メディアクエリ用のクラスを指定 |
| `loading` | ローディング時のグリッド領域 |
| `error` | エラー表示領域 |
| `empty` | 空状態表示領域 |

## カスタマイズ

### classNames

ヘッダー・タイトル・グリッドなど、各パーツに `classNames` でクラスを追加できます。

```tsx
<VcTypeList
  title="スキル"
  totalCount={5}
  classNames={{
    header: "my-section-header",
    title: "my-section-title",
    grid: "my-vc-grid",
  }}
>
  {skillCards}
</VcTypeList>
```

### i18n

`labels` で文言を差し替えできます。

```tsx
<VcTypeList
  title="Skills"
  totalCount={5}
  labels={{
    totalLabel: "Total acquired",
    totalUnit: "",
  }}
>
  {skillCards}
</VcTypeList>
```

---

# SkillCard

スキル用のカードです。未獲得/獲得状態、獲得数やプログレスを表示します。

## インポート

```tsx
import { SkillCard } from "@shomia/ui-sdk-react";
import type { SkillCardProps } from "@shomia/ui-sdk-react";
```

## 基本的な使い方

```tsx
<SkillCard
  title="React 基礎"
  description="React の基本を理解している"
  imageUrl="/images/react.png"
  imageAlt="React 基礎"
  isAcquired={true}
  maxNum={5}
  acquiredCount={3}
  href="/events/ev1/vc/vc1"
/>
```

## Props

| Prop | 型 | 必須 | 説明 |
|------|-----|------|------|
| `title` | `string` | Yes | タイトル |
| `description` | `string` | - | 説明文 |
| `imageUrl` | `string \| null` | - | 画像 URL |
| `imageAlt` | `string` | - | 画像の代替テキスト |
| `isAcquired` | `boolean` | Yes | 獲得済みか |
| `maxNum` | `number \| null` | - | 最大獲得数（ある場合プログレス表示） |
| `acquiredCount` | `number` | Yes | 獲得数 |
| `labels` | [`SkillCardLabels`](#skillcardlabels) | - | ラベル文言の上書き（i18n 用） |
| `onItemClick` | `(event: React.MouseEvent<HTMLElement>) => void` | - | クリック時のコールバック。指定時は href より優先 |
| `href` | `string \| null` | - | リンク先 |

## 型定義

### SkillCardLabels

| キー | デフォルト値 | 説明 |
|------|------------|------|
| `unacquired` | `"未獲得"` | 未獲得時表示 |
| `acquiredCount` | `"獲得数"` | 獲得数ラベル（「獲得数: N」） |

## カスタマイズ

### CSS 変数

SkillCard と TicketCard で共通のリストカード用 CSS 変数です。親要素や `:root` で上書きできます。

```css
:root {
  --shomia-vc-list-card-radius: 12px;
  --shomia-vc-list-card-min-height: 260px;
  --shomia-vc-list-card-image-size: 64px;
}
```

<details>
<summary>全 CSS 変数一覧（リストカード共通）</summary>

| 変数名 | デフォルト値 |
|--------|------------|
| `--shomia-vc-list-card-radius` | `8px` |
| `--shomia-vc-list-card-min-height` | `240px` |
| `--shomia-vc-list-card-image-size` | `56px` |
| `--shomia-vc-list-card-image-size-sm` | `72px` |
| `--shomia-vc-list-card-image-size-lg` | `88px` |
| `--shomia-vc-list-card-no-image-color` | `#6b7280` |
| `--shomia-vc-list-card-no-image-bg` | `#e5e7eb` |
| `--shomia-vc-list-card-placeholder-bg` | `#e5e7eb` |

</details>

### i18n

`labels` で文言を差し替えできます。

```tsx
<SkillCard
  title="React Basics"
  isAcquired={false}
  acquiredCount={0}
  labels={{ unacquired: "Not acquired", acquiredCount: "Count" }}
/>
```

---

# TicketCard

チケット用のカードです。有効期限の表示と期限切れ判定を行います。

## インポート

```tsx
import { TicketCard } from "@shomia/ui-sdk-react";
import type { TicketCardProps } from "@shomia/ui-sdk-react";
```

## 基本的な使い方

```tsx
<TicketCard
  title="カンファレンス参加券"
  description="2025年度技術カンファレンス"
  imageUrl="/images/ticket.png"
  imageAlt="参加券"
  expiredAt="2025-12-31"
  href="/events/ev1/vc/vc2"
/>
```

## Props

| Prop | 型 | 必須 | 説明 |
|------|-----|------|------|
| `title` | `string` | Yes | タイトル |
| `description` | `string` | - | 説明文 |
| `imageUrl` | `string \| null` | - | 画像 URL |
| `imageAlt` | `string` | - | 画像の代替テキスト |
| `expiredAt` | `string \| null` | - | 有効期限（ISO 文字列等） |
| `formatExpiredAt` | `(iso: string) => string` | - | 有効期限の表示フォーマット。未指定時は年月日のみのデフォルト表示 |
| `labels` | [`TicketCardLabels`](#ticketcardlabels) | - | ラベル文言の上書き（i18n 用） |
| `onItemClick` | `(event: React.MouseEvent<HTMLElement>) => void` | - | クリック時のコールバック。指定時は href より優先 |
| `href` | `string \| null` | - | リンク先 |

## 型定義

### TicketCardLabels

| キー | デフォルト値 | 説明 |
|------|------------|------|
| `expiresAt` | `"有効期限"` | 有効期限ラベル |
| `expired` | `"期限切れ"` | 期限切れ表示 |
| `notSet` | `"未設定"` | 有効期限未設定時 |

## カスタマイズ

### CSS 変数

SkillCard と共通です。上記 [SkillCard](#skillcard) の「カスタマイズ」内 CSS 変数を参照してください。

### 有効期限のフォーマット

`formatExpiredAt` で表示形式を差し替えできます。未指定時は年月日のみ（例: 2026年1月15日）です。

```tsx
<TicketCard
  title="参加券"
  expiredAt="2025-12-31T23:59:59Z"
  formatExpiredAt={(iso) => new Date(iso).toLocaleDateString("ja-JP")}
/>
```

### i18n

`labels` で文言を差し替えできます。

```tsx
<TicketCard
  title="Ticket"
  expiredAt="2025-12-31"
  labels={{
    expiresAt: "Expires",
    expired: "Expired",
    notSet: "Not set",
  }}
/>
```

---

# BadgeItem

バッジ用のアイテムです。獲得数と「N件取得」/「未獲得」を表示します。

## インポート

```tsx
import { BadgeItem } from "@shomia/ui-sdk-react";
import type { BadgeItemProps } from "@shomia/ui-sdk-react";
```

## 基本的な使い方

```tsx
<BadgeItem
  title="ハッカソン優勝"
  imageUrl="/images/badge.png"
  imageAlt="ハッカソン優勝"
  isAcquired={true}
  acquiredCount={1}
  href="/events/ev1/vc/vc3"
/>
```

## Props

| Prop | 型 | 必須 | 説明 |
|------|-----|------|------|
| `title` | `string` | Yes | タイトル |
| `imageUrl` | `string \| null` | - | 画像 URL |
| `imageAlt` | `string` | - | 画像の代替テキスト |
| `isAcquired` | `boolean` | Yes | 獲得済みか |
| `acquiredCount` | `number` | Yes | 獲得数 |
| `labels` | [`BadgeItemLabels`](#badgeitemlabels) | - | ラベル文言の上書き（i18n 用） |
| `onItemClick` | `(event: React.MouseEvent<HTMLElement>) => void` | - | クリック時のコールバック。指定時は href より優先 |
| `href` | `string \| null` | - | リンク先 |

## 型定義

### BadgeItemLabels

| キー | デフォルト値 | 説明 |
|------|------------|------|
| `acquiredSuffix` | `"件取得"` | 獲得時の単位（N の後ろ。「3件取得」の「件取得」） |
| `unacquired` | `"未獲得"` | 未獲得時表示 |

## カスタマイズ

### CSS 変数

BadgeItem と AdminBadgeItem で共通のバッジ用 CSS 変数です。親要素や `:root` で上書きできます。

```css
:root {
  --shomia-badge-vc-item-image-size: 96px;
  --shomia-badge-vc-item-count-bg: #dc2626;
}
```

<details>
<summary>全 CSS 変数一覧（バッジ共通）</summary>

| 変数名 | デフォルト値 |
|--------|------------|
| `--shomia-badge-vc-item-image-size` | `80px` |
| `--shomia-badge-vc-item-image-size-sm` | `112px` |
| `--shomia-badge-vc-item-image-size-md` | `160px` |
| `--shomia-badge-vc-item-image-size-lg` | `208px` |
| `--shomia-badge-vc-item-count-bg` | `#ef4444` |

</details>

### i18n

`labels` で文言を差し替えできます。

```tsx
<BadgeItem
  title="Hackathon Winner"
  isAcquired={true}
  acquiredCount={2}
  labels={{ acquiredSuffix: " acquired", unacquired: "Not acquired" }}
/>
```

---

# AdminBadgeItem

管理者バッジ用のアイテムです。画像右上に BadgeCheck 風アイコンを表示します。

## インポート

```tsx
import { AdminBadgeItem } from "@shomia/ui-sdk-react";
import type { AdminBadgeItemProps } from "@shomia/ui-sdk-react";
```

## 基本的な使い方

```tsx
<AdminBadgeItem
  title="プロジェクトリーダー認定"
  description="成果を出したことを証明するバッジ"
  imageUrl="/images/admin-badge.png"
  imageAlt="プロジェクトリーダー認定"
  href="/events/ev1/vc/vc4"
/>
```

## Props

| Prop | 型 | 必須 | 説明 |
|------|-----|------|------|
| `title` | `string` | Yes | タイトル |
| `description` | `string` | - | 説明文 |
| `imageUrl` | `string \| null` | - | 画像 URL |
| `imageAlt` | `string` | - | 画像の代替テキスト |
| `onItemClick` | `(event: React.MouseEvent<HTMLElement>) => void` | - | クリック時のコールバック。指定時は href より優先 |
| `href` | `string \| null` | - | リンク先 |

## カスタマイズ

### CSS 変数

BadgeItem と共通です。上記 [BadgeItem](#badgeitem) の「カスタマイズ」内 CSS 変数を参照してください。

> 各カード（SkillCard / TicketCard / BadgeItem / AdminBadgeItem）は `React.HTMLAttributes<HTMLDivElement>` を継承するため、`className` や `id`、`style`、`data-*` なども指定できます。

---

# VcIssuedCard

発行済みVC一覧用のカードです。VcDetailCard と同様に `vc` オブジェクトで表示データを渡します。タイプバッジ、画像、タイトル、受取者、発行日時を表示します。

## インポート

```tsx
import { VcIssuedCard } from "@shomia/ui-sdk-react";
import type { VcIssuedCardProps, VcIssuedView } from "@shomia/ui-sdk-react";
```

## 基本的な使い方

```tsx
<VcIssuedCard
  vc={{
    type: "Skill",
    title: "React 基礎",
    imageUrl: "/images/react.png",
    imageAlt: "React 基礎",
    holderName: "山田太郎",
    issuedAt: "2025-01-15T10:00:00.000Z",
  }}
  href="/events/ev1/vc/vc1"
/>
```

## Props

| Prop | 型 | 必須 | 説明 |
|------|-----|------|------|
| `vc` | `VcIssuedView` | Yes | 表示用データ（type, title, imageUrl?, imageAlt?, holderName, issuedAt） |
| `formatDate` | `(iso: string) => string` | - | 発行日時のフォーマット関数。未指定時は `toLocaleString("ja-JP")` |
| `labels` | `VcIssuedCardLabels` | - | ラベル文言の上書き（i18n 用。`holder` で受取者ラベルを変更可能） |
| `classNames` | `VcIssuedCardClassNames` | - | 各パーツに追加するクラス名 |
| `onItemClick` | `(event) => void` | - | クリック時のコールバック。指定時は href より優先 |
| `href` | `string \| null` | - | リンク先 |

> `React.HTMLAttributes<HTMLDivElement>` を継承しているため、`id`, `style`, `data-*` 等も指定可能です。

## 型定義

### VcIssuedView

| キー | 型 | 説明 |
|------|-----|------|
| `type` | `VcListType` | VC種別（Skill / Ticket / Badge / AdminBadge） |
| `title` | `string` | タイトル |
| `imageUrl` | `string \| null` | 画像 URL |
| `imageAlt` | `string` | 画像の代替テキスト |
| `holderName` | `string` | 受取者名 |
| `issuedAt` | `string` | 発行日時の ISO 文字列 |

### VcIssuedCardLabels

| キー | デフォルト値 | 説明 |
|------|------------|------|
| `typeSkill` | `"スキル"` | `type` が `"Skill"` のときのバッジ文言 |
| `typeBadge` | `"バッジ"` | `type` が `"Badge"` のときのバッジ文言 |
| `typeTicket` | `"チケット"` | `type` が `"Ticket"` のときのバッジ文言 |
| `typeAdminBadge` | `"管理者バッジ"` | `type` が `"AdminBadge"` のときのバッジ文言 |
| `holder` | `"受取者"` | 受取者ラベル（`受取者: 山田太郎` の「受取者」部分） |

### VcIssuedCardClassNames

| キー | 対象 |
|------|------|
| `root` | カード本体のラッパー要素 |
| `badge` | タイプバッジ部分（右上のラベル） |
| `body` | 画像・タイトル・受取者・発行日時を含む本体部分 |
| `image` | 画像要素（`<img>`） |
| `title` | タイトルテキスト |
| `holder` | 受取者テキスト |
| `issuedAt` | 発行日時テキスト |

## カスタマイズ

### CSS 変数

```css
:root {
  --shomia-vc-issued-card-radius: 8px;
  --shomia-vc-issued-card-border-color: #d1d5db;
  --shomia-vc-issued-card-bg: #f6f8fd;
}
```

<details>
<summary>全 CSS 変数一覧</summary>

| 変数名 | デフォルト値 |
|--------|------------|
| `--shomia-vc-issued-card-radius` | `8px` |
| `--shomia-vc-issued-card-border-color` | `#d1d5db` |
| `--shomia-vc-issued-card-bg` | `#f6f8fd` |
| `--shomia-vc-issued-card-padding` | `16px` |
| `--shomia-vc-issued-card-padding-sm` | `24px` |
| `--shomia-vc-issued-card-gap` | `16px` |
| `--shomia-vc-issued-card-gap-sm` | `20px` |
| `--shomia-vc-issued-card-image-size` | `64px` |
| `--shomia-vc-issued-card-image-size-sm` | `80px` |
| `--shomia-vc-issued-card-image-wrap-bg` | `transparent` |
| `--shomia-vc-issued-card-no-image-bg` | `#e5e7eb` |
| `--shomia-vc-issued-card-no-image-color` | `#6b7280` |
| `--shomia-vc-issued-card-title-color` | `#111827` |
| `--shomia-vc-issued-card-text-color` | `#374151` |

</details>

### 発行日時のフォーマット

`formatDate` で表示形式を差し替えできます。未指定時は `toLocaleString("ja-JP")` です。

```tsx
import { format } from "date-fns";

<VcIssuedCard
  vc={{
    type: "Skill",
    title: "React 基礎",
    holderName: "山田太郎",
    issuedAt: vcItem.createdAt,
  }}
  formatDate={(iso) => format(new Date(iso), "yyyy/MM/dd HH:mm")}
/>
```

---

# VcIssuedList

発行済みVCを **イベントごとに 1 つ** 使う details/summary のリストです。VcTypeList と同様に、`title`・`count`・`children` で 1 イベント分の見出しとカードを渡します。複数イベントのときは、イベントごとに `VcIssuedList` を並べてください。

## インポート

```tsx
import { VcIssuedList } from "@shomia/ui-sdk-react";
import type { VcIssuedListProps } from "@shomia/ui-sdk-react";
```

## 基本的な使い方

```tsx
{/* イベントごとに VcIssuedList を1つ */}
{grouped.map(({ event, items }) => (
  <VcIssuedList
    key={event.id}
    title={event.name}
    count={items.length}
  >
    {items.map((item) => (
      <VcIssuedCard key={item.vcItem.id} vc={{ ... }} />
    ))}
  </VcIssuedList>
))}
```

## 状態表示

### 空状態

`children` が空のときに表示する内容を `empty` で上書きできます。未指定時は `labels.empty`（デフォルト: `"発行したVCがありません"`）を表示します。

```tsx
<VcIssuedList
  title="イベントA"
  count={0}
  empty={<p>まだ発行済み VC がありません</p>}
>
  {[]}
</VcIssuedList>
```

## Props

| Prop | 型 | 必須 | 説明 |
|------|-----|------|------|
| `title` | `string` | Yes | セクション見出し（例: イベント名） |
| `count` | `number` | Yes | 件数表示用の数値 |
| `children` | `ReactNode` | Yes | グリッド内に並べるカード（例: `VcIssuedCard` の並び） |
| `labels` | `VcIssuedListLabels` | - | 空状態テキスト・件数サフィックス（i18n 用） |
| `classNames` | `VcIssuedListClassNames` | - | 各パーツに追加するクラス名 |
| `defaultOpen` | `boolean` | - | details の初期の開閉状態。デフォルトは true |
| `empty` | `ReactNode` | - | children が空のときの表示（未指定時は `labels.empty`） |

> `React.HTMLAttributes<HTMLDivElement>` を継承しているため、`className`・`id`・`data-*` なども指定可能です。

## 型定義

### VcIssuedListLabels

| キー | デフォルト値 | 説明 |
|------|------------|------|
| `empty` | `"発行したVCがありません"` | children が空のときに表示する文言 |
| `countSuffix` | `"件"` | 件数の後ろに付与する単位（例: `3件`） |

### VcIssuedListClassNames

| キー | 対象 |
|------|------|
| `root` | ルート要素（リスト全体のラッパー） |
| `group` | `<details>` 要素（1 セクション全体） |
| `summary` | セクション見出し行（タイトル＋件数＋アイコンの行） |
| `summaryIcon` | 開閉を示すアイコン（Chevron） |
| `title` | セクションタイトル部分 |
| `count` | 件数表示部分 |
| `content` | グリッドを包むコンテナ |
| `grid` | カードを並べるグリッド（列数などを利用側で指定） |
| `empty` | 空状態の表示領域 |

## カスタマイズ

### classNames

ヘッダー行やタイトル文字色、グリッド列数などを、`classNames` で任意の CSS クラスに差し替えできます。

```tsx
<VcIssuedList
  title="イベントA"
  count={3}
  classNames={{
    summary: "my-issued-list-summary",
    title: "my-issued-list-title",
    grid: "my-issued-list-grid",
  }}
>
  {items.map((item) => (
    <VcIssuedCard key={item.vcItem.id} vc={{ ... }} />
  ))}
</VcIssuedList>
```

### CSS 変数

```css
:root {
  --shomia-vc-issued-list-gap: 16px;
  --shomia-vc-issued-list-summary-border-color: #9ca3af;
  --shomia-vc-issued-list-summary-color: #111827;
}
```

<details>
<summary>全 CSS 変数一覧</summary>

| 変数名 | デフォルト値 |
|--------|------------|
| `--shomia-vc-issued-list-gap` | `16px` |
| `--shomia-vc-issued-list-summary-border-color` | `#9ca3af` |
| `--shomia-vc-issued-list-summary-color` | `#111827` |
| `--shomia-vc-issued-list-summary-hover-color` | `#4b5563` |
| `--shomia-vc-issued-list-count-font-size` | `0.875rem` |
| `--shomia-vc-issued-list-grid-max-width` | `80rem` |
| `--shomia-vc-issued-list-grid-gap` | `12px` |
| `--shomia-vc-issued-list-grid-gap-sm` | `24px` |
| `--shomia-vc-issued-list-empty-color` | `#6b7280` |

</details>

---
# 組み合わせ例

VcDetailCard と VcHistory を組み合わせて VC 詳細画面を構築する例です。

```tsx
import { VcDetailCard, VcHistory } from "@shomia/ui-sdk-react";
import "@shomia/ui-sdk-react/styles.css";

function VcDetailPage({ vc }) {
  const detailView = {
    id: vc.id,
    type: vc.vcDefinition.type,
    title: vc.vcDefinition.name,
    description: vc.vcDefinition.description,
    imageUrl: vc.vcDefinition.imageUrl,
    ...(vc.vcDefinition.type !== "Ticket"
      ? { acquiredCount: vc.vcItems.length, maxNum: vc.vcDefinition.maxNum }
      : { expiredAt: vc.vcItems[0]?.expiredAt }),
  };

  const historyItems = vc.vcItems.map((item) => ({
    id: item.id,
    issuer: { id: item.issuerId, name: item.issuer?.name ?? item.issuerId },
    holder: { id: vc.holderId, name: vc.holder?.name ?? vc.holderId },
    createdAt: item.createdAt,
    expiredAt: item.expiredAt,
  }));

  return (
    <div style={{ maxWidth: 400, margin: "0 auto" }}>
      <VcDetailCard vc={detailView} />
      <h2>獲得履歴</h2>
      <VcHistory items={historyItems} />
    </div>
  );
}
```

VcTypeListとSkillCard・TicketCard・BadgeItem を組み合わせて VC一覧を構築する例です。

```tsx
import {
  VcTypeList,
  SkillCard,
  TicketCard,
  BadgeItem,
  AdminBadgeItem,
} from "@shomia/ui-sdk-react";
import "@shomia/ui-sdk-react/styles.css";

function EventVcList({ skills, tickets, badges, adminBadges }) {
  return (
    <>
      <VcTypeList
        title="スキル"
        totalCount={skills.reduce((s, i) => s + i.acquiredCount, 0)}
        classNames={{
          header: "skill-section-header",
          title: "skill-section-title",
          grid: "my-skill-grid",
        }}
      >
        {skills.map((item) => (
          <SkillCard
            key={item.id}
            title={item.title}
            description={item.description}
            imageUrl={item.imageUrl}
            isAcquired={item.isAcquired}
            maxNum={item.maxNum}
            acquiredCount={item.acquiredCount}
            href={item.href}
          />
        ))}
      </VcTypeList>

      <VcTypeList
        title="チケット"
        totalCount={tickets.length}
        classNames={{
          header: "ticket-section-header",
          title: "ticket-section-title",
          grid: "my-ticket-grid",
        }}
      >
        {tickets.map((item) => (
          <TicketCard
            key={item.id}
            title={item.title}
            description={item.description}
            imageUrl={item.imageUrl}
            expiredAt={item.expiredAt}
            href={item.href}
          />
        ))}
      </VcTypeList>

      <VcTypeList
        title="バッジ"
        totalCount={badges.reduce((s, i) => s + i.acquiredCount, 0)}
        classNames={{
          header: "badge-section-header",
          title: "badge-section-title",
          grid: "my-badge-grid",
        }}
      >
        {adminBadges.map((item) => (
          <AdminBadgeItem
            key={item.id}
            title={item.title}
            description={item.description}
            imageUrl={item.imageUrl}
            href={item.href}
          />
        ))}
        {badges.map((item) => (
          <BadgeItem
            key={item.id}
            title={item.title}
            imageUrl={item.imageUrl}
            isAcquired={item.isAcquired}
            acquiredCount={item.acquiredCount}
            href={item.href}
          />
        ))}
      </VcTypeList>
    </>
  );
}
```

---

# 型のインポート

```typescript
import type {
  VcType,
  VcDetailView,
  VcDetailCardProps,
  VcDetailCardLabels,
  VcDetailCardClassNames,
  VcHistoryItem,
  VcHistoryProps,
  VcHistoryLabels,
  VcHistoryClassNames,
  VcIssuerInfo,
  VcHolderInfo,
  VcTypeListProps,
  VcTypeListClassNames,
  VcTypeListLabels,
  SkillCardProps,
  SkillCardLabels,
  TicketCardProps,
  TicketCardLabels,
  BadgeItemProps,
  BadgeItemLabels,
  AdminBadgeItemProps,
  VcListType,
} from "@shomia/ui-sdk-react";
```

---

# 開発

```bash
# ビルド
pnpm --filter @shomia/ui-sdk-react build

# ウォッチモード
pnpm --filter @shomia/ui-sdk-react build:watch
```