# @legionhandtech/lht-react-widgets

Embeddable Legion widgets for React applications. Each widget renders in an isolated iframe — no style conflicts, no CORS issues, no extra dependencies.

## Install

```bash
npm install @legionhandtech/lht-react-widgets
```

React 17+ is a peer dependency (only needed for the React components — web component imports work without React).

## Quick Start

```tsx
import { LegionOpportunities } from "@legionhandtech/lht-react-widgets";

function App() {
  return (
    <LegionOpportunities
      baseUrl="https://your-legion-domain.com"
      count={5}
      layout="horizontal"
      theme="light"
    />
  );
}
```

## Widgets

### LegionOpportunities

Displays opportunity cards in a horizontal or vertical layout.

```tsx
import { LegionOpportunities } from "@legionhandtech/lht-react-widgets";

<LegionOpportunities
  baseUrl="https://your-legion-domain.com"
  count={5}
  layout="horizontal"
  theme="dark"
  tags="featured,new"
  height="400px"
  apiKey="your-api-key"
  onLoad={(detail) => console.log("Loaded", detail)}
  onError={(msg) => console.error(msg)}
/>;
```

| Prop      | Type                                                                   | Default        | Description                      |
| --------- | ---------------------------------------------------------------------- | -------------- | -------------------------------- |
| `baseUrl` | `string`                                                               | **required**   | Base URL of your Legion instance |
| `count`   | `number`                                                               | `5`            | Number of opportunities (1–20)   |
| `layout`  | `"horizontal" \| "vertical"`                                           | `"horizontal"` | Card layout direction            |
| `tags`    | `string`                                                               | —              | Comma-separated tag filter       |
| `theme`   | `"light" \| "dark" \| "minimal" \| "bold" \| "corporate" \| "vibrant"` | `"light"`      | Preset theme                     |
| `height`  | `string`                                                               | `"auto"`       | Widget height CSS value          |
| `apiKey`  | `string`                                                               | —              | API key for authentication       |

---

### LegionCommunityGallery

A media gallery showcasing community content.

```tsx
import { LegionCommunityGallery } from "@legionhandtech/lht-react-widgets";

<LegionCommunityGallery
  baseUrl="https://your-legion-domain.com"
  count={4}
  theme="light"
  communityUrl="/community"
/>;
```

| Prop           | Type                | Default      | Description                      |
| -------------- | ------------------- | ------------ | -------------------------------- |
| `baseUrl`      | `string`            | **required** | Base URL of your Legion instance |
| `count`        | `number`            | `4`          | Number of gallery items (1–12)   |
| `theme`        | `"light" \| "dark"` | `"light"`    | Preset theme                     |
| `showViewAll`  | `boolean`           | `true`       | Show "View all" link             |
| `viewAllUrl`   | `string`            | `"/gallery"` | URL for the "View all" link      |
| `communityUrl` | `string`            | —            | URL when clicking a media item   |
| `height`       | `string`            | `"auto"`     | Widget height CSS value          |

---

### LegionQA

An interactive Q&A / knowledge-base widget.

```tsx
import { LegionQA } from "@legionhandtech/lht-react-widgets";

<LegionQA
  baseUrl="https://your-legion-domain.com"
  authToken="jwt-token"
  productId="prod-123"
  theme="light"
/>;
```

| Prop            | Type                | Default       | Description                          |
| --------------- | ------------------- | ------------- | ------------------------------------ |
| `baseUrl`       | `string`            | **required**  | Base URL of your Legion instance     |
| `authToken`     | `string`            | —             | JWT token for authenticated features |
| `productId`     | `string`            | —             | Scope questions to a product         |
| `username`      | `string`            | `"Anonymous"` | Display name                         |
| `pageSize`      | `number`            | `5`           | Items per page                       |
| `theme`         | `"light" \| "dark"` | `"light"`     | Preset theme                         |
| `showSearch`    | `boolean`           | `true`        | Show search bar                      |
| `showAskButton` | `boolean`           | `true`        | Show "Ask a Question" button         |
| `width`         | `string`            | `"600px"`     | Widget width CSS value               |
| `height`        | `string`            | `"800px"`     | Widget height CSS value              |

---

### LegionSocial

A social feed widget with grid and feed layouts.

```tsx
import { LegionSocial } from "@legionhandtech/lht-react-widgets";

<LegionSocial
  baseUrl="https://your-legion-domain.com"
  layout="feed"
  theme="dark"
  channel="general"
/>;
```

| Prop           | Type                            | Default      | Description                      |
| -------------- | ------------------------------- | ------------ | -------------------------------- |
| `baseUrl`      | `string`                        | **required** | Base URL of your Legion instance |
| `layout`       | `"feed" \| "grid" \| "compact"` | `"feed"`     | Layout style                     |
| `theme`        | `"light" \| "dark"`             | `"light"`    | Preset theme                     |
| `rows`         | `number`                        | `3`          | Number of rows (grid layout)     |
| `cols`         | `number`                        | `3`          | Number of columns (grid layout)  |
| `communityUrl` | `string`                        | —            | Link to full community page      |
| `channel`      | `string`                        | —            | Filter to a specific channel     |
| `authToken`    | `string`                        | —            | JWT token                        |
| `userId`       | `string`                        | —            | Current user ID                  |
| `username`     | `string`                        | —            | Current user display name        |
| `height`       | `string`                        | `"800"`      | Widget height CSS value          |

---

### LegionWallet

Displays a user's wallet balance and transaction history.

```tsx
import { LegionWallet } from "@legionhandtech/lht-react-widgets";

<LegionWallet
  baseUrl="https://your-legion-domain.com"
  userId="user-123"
  apiKey="your-api-key"
  theme="light"
/>;
```

| Prop          | Type                | Default      | Description                      |
| ------------- | ------------------- | ------------ | -------------------------------- |
| `baseUrl`     | `string`            | **required** | Base URL of your Legion instance |
| `userId`      | `string`            | —            | User ID to display wallet for    |
| `apiKey`      | `string`            | —            | API key for authentication       |
| `theme`       | `"light" \| "dark"` | `"light"`    | Preset theme                     |
| `showBalance` | `boolean`           | —            | Show the balance section         |
| `showHistory` | `boolean`           | —            | Show the transaction history     |
| `height`      | `string`            | `"500px"`    | Widget height CSS value          |

---

### LegionMatrix

A matrix / leaderboard display widget.

```tsx
import { LegionMatrix } from "@legionhandtech/lht-react-widgets";

<LegionMatrix
  baseUrl="https://your-legion-domain.com"
  theme="dark"
  emptyTitle="No data yet"
  height="600px"
/>;
```

| Prop         | Type                | Default      | Description                      |
| ------------ | ------------------- | ------------ | -------------------------------- |
| `baseUrl`    | `string`            | **required** | Base URL of your Legion instance |
| `theme`      | `"light" \| "dark"` | `"light"`    | Preset theme                     |
| `emptyTitle` | `string`            | —            | Title shown when no data         |
| `emptyIcon`  | `string`            | —            | Icon shown when no data          |
| `height`     | `string`            | `"600px"`    | Widget height CSS value          |

---

### Profile Widget

```tsx
import { LegionProfile } from "@legionhandtech/lht-react-widgets";

function App() {
  return (
    <LegionProfile
      baseUrl="https://your-legion-domain.com"
      authToken="your-jwt-token"
      theme="light"
    />
  );
}
```

| Prop        | Type                | Default      | Description                                                        |
| ----------- | ------------------- | ------------ | ------------------------------------------------------------------ |
| `baseUrl`   | `string`            | **required** | Base URL of your Legion instance                                   |
| `authToken` | `string`            | —            | JWT token for authenticated profile viewing (sent via postMessage) |
| `theme`     | `"light" \| "dark"` | `"light"`    | Preset theme                                                       |
| `width`     | `string`            | `"400px"`    | Widget width CSS value                                             |
| `height`    | `string`            | `"400px"`    | Widget height CSS value                                            |

---

### Profile Edit Widget

```tsx
import { LegionProfileEdit } from "@legionhandtech/lht-react-widgets";

function App() {
  return (
    <LegionProfileEdit
      baseUrl="https://your-legion-domain.com"
      authToken="your-jwt-token"
      theme="light"
    />
  );
}
```

| Prop        | Type                | Default      | Description                                                        |
| ----------- | ------------------- | ------------ | ------------------------------------------------------------------ |
| `baseUrl`   | `string`            | **required** | Base URL of your Legion instance                                   |
| `authToken` | `string`            | —            | JWT token for authenticated profile editing (sent via postMessage) |
| `theme`     | `"light" \| "dark"` | `"light"`    | Preset theme                                                       |
| `width`     | `string`            | `"600px"`    | Widget width CSS value                                             |
| `height`    | `string`            | `"800px"`    | Widget height CSS value                                            |

---

### Notifications Widget

```tsx
import { LegionNotifications } from "@legionhandtech/lht-react-widgets";

function App() {
  return (
    <LegionNotifications
      baseUrl="https://your-legion-domain.com"
      authToken="your-jwt-token"
      userId="user-42"
      theme="light"
      maxNotifications={10}
    />
  );
}
```

| Prop               | Type                | Default      | Description                                               |
| ------------------ | ------------------- | ------------ | --------------------------------------------------------- |
| `baseUrl`          | `string`            | **required** | Base URL of your Legion instance                          |
| `authToken`        | `string`            | —            | JWT token for authenticated access (sent via postMessage) |
| `userId`           | `string`            | —            | User ID for notification lookup                           |
| `theme`            | `"light" \| "dark"` | `"light"`    | Preset theme                                              |
| `maxNotifications` | `number`            | `10`         | Maximum notifications to display                          |
| `autoRefresh`      | `boolean`           | `true`       | Enable auto-refresh (every 30s)                           |
| `width`            | `string`            | `"100%"`     | Widget width CSS value                                    |
| `height`           | `string`            | `"400px"`    | Widget height CSS value                                   |

---

### Saved Opportunities Widget

```tsx
import { LegionSavedOpportunities } from "@legionhandtech/lht-react-widgets";

function App() {
  return (
    <LegionSavedOpportunities
      baseUrl="https://your-legion-domain.com"
      authToken="your-jwt-token"
      theme="light"
    />
  );
}
```

| Prop        | Type                | Default      | Description                                               |
| ----------- | ------------------- | ------------ | --------------------------------------------------------- |
| `baseUrl`   | `string`            | **required** | Base URL of your Legion instance                          |
| `authToken` | `string`            | —            | JWT token for authenticated access (sent via postMessage) |
| `theme`     | `"light" \| "dark"` | `"light"`    | Preset theme                                              |
| `width`     | `string`            | `"100%"`     | Widget width CSS value                                    |
| `height`    | `string`            | `"400px"`    | Widget height CSS value                                   |

### Campaigns Carousel

```tsx
import { LegionCampaignsCarousel } from "@legionhandtech/lht-react-widgets";

function App() {
  return (
    <LegionCampaignsCarousel
      baseUrl="https://your-legion-domain.com"
      maxCampaigns={5}
      theme="light"
    />
  );
}
```

| Prop             | Type                | Default      | Description                      |
| ---------------- | ------------------- | ------------ | -------------------------------- |
| `baseUrl`        | `string`            | **required** | Base URL of your Legion instance |
| `apiKey`         | `string`            | —            | API key for authorization        |
| `maxCampaigns`   | `number`            | `5`          | Maximum campaigns to show        |
| `showExpiration` | `boolean`           | `true`       | Show expiration dates            |
| `theme`          | `"light" \| "dark"` | `"light"`    | Preset theme                     |
| `height`         | `number`            | `280`        | Widget height in pixels          |

### Community Groups

```tsx
import { LegionCommunityGroups } from "@legionhandtech/lht-react-widgets";

function App() {
  return (
    <LegionCommunityGroups
      baseUrl="https://your-legion-domain.com"
      count={6}
      layout="grid"
      theme="light"
    />
  );
}
```

| Prop      | Type                                  | Default      | Description                        |
| --------- | ------------------------------------- | ------------ | ---------------------------------- |
| `baseUrl` | `string`                              | **required** | Base URL of your Legion instance   |
| `count`   | `number`                              | `6`          | Number of groups to display (1–20) |
| `layout`  | `"grid" \| "list"`                    | `"grid"`     | Layout mode                        |
| `theme`   | `"light" \| "dark"`                   | `"light"`    | Preset theme                       |
| `sortBy`  | `"trending" \| "popular" \| "recent"` | `"trending"` | Sort order                         |

### Community Header

```tsx
import { LegionCommunityHeader } from "@legionhandtech/lht-react-widgets";

function App() {
  return (
    <LegionCommunityHeader
      baseUrl="https://your-legion-domain.com"
      title="My Community"
      theme="light"
    />
  );
}
```

| Prop         | Type                | Default       | Description                      |
| ------------ | ------------------- | ------------- | -------------------------------- |
| `baseUrl`    | `string`            | **required**  | Base URL of your Legion instance |
| `title`      | `string`            | `"Community"` | Community title                  |
| `showSearch` | `boolean`           | `true`        | Show search bar                  |
| `showStats`  | `boolean`           | `true`        | Show stats section               |
| `theme`      | `"light" \| "dark"` | `"light"`     | Preset theme                     |

### Community Posts

```tsx
import { LegionCommunityPosts } from "@legionhandtech/lht-react-widgets";

function App() {
  return (
    <LegionCommunityPosts
      baseUrl="https://your-legion-domain.com"
      authToken="your-jwt-token"
      pageSize={10}
      theme="light"
    />
  );
}
```

| Prop        | Type                                    | Default      | Description                                               |
| ----------- | --------------------------------------- | ------------ | --------------------------------------------------------- |
| `baseUrl`   | `string`                                | **required** | Base URL of your Legion instance                          |
| `authToken` | `string`                                | —            | JWT token for authenticated access (sent via postMessage) |
| `pageSize`  | `number`                                | `10`         | Posts per page (5–50)                                     |
| `filter`    | `"recent" \| "popular" \| "unanswered"` | `"recent"`   | Filter mode                                               |
| `theme`     | `"light" \| "dark"`                     | `"light"`    | Preset theme                                              |

### Community Sidebar

```tsx
import { LegionCommunitySidebar } from "@legionhandtech/lht-react-widgets";

function App() {
  return (
    <LegionCommunitySidebar
      baseUrl="https://your-legion-domain.com"
      title="Getting Started"
      theme="light"
    />
  );
}
```

| Prop             | Type                | Default             | Description                      |
| ---------------- | ------------------- | ------------------- | -------------------------------- |
| `baseUrl`        | `string`            | **required**        | Base URL of your Legion instance |
| `title`          | `string`            | `"Getting Started"` | Sidebar title                    |
| `showGuidelines` | `boolean`           | `true`              | Show community guidelines link   |
| `showFaqs`       | `boolean`           | `true`              | Show FAQs link                   |
| `theme`          | `"light" \| "dark"` | `"light"`           | Preset theme                     |

---

### LegionPioneerPointStatus

Compact card that shows the authenticated user's current pioneer point balance and status. Auth is sent via `postMessage` (never via URL).

```tsx
import { LegionPioneerPointStatus } from "@legionhandtech/lht-react-widgets";

<LegionPioneerPointStatus
  baseUrl="https://your-legion-domain.com"
  authToken="your-jwt-token"
  theme="light"
  height="180px"
/>;
```

| Prop        | Type                | Default      | Description                                   |
| ----------- | ------------------- | ------------ | --------------------------------------------- |
| `baseUrl`   | `string`            | **required** | Base URL of your Legion instance              |
| `authToken` | `string`            | —            | JWT (relayed via `postMessage`, never as URL) |
| `theme`     | `"light" \| "dark"` | `"light"`    | Preset theme                                  |
| `width`     | `string`            | `"100%"`     | Container width                               |
| `height`    | `string`            | `"180px"`    | Container height                              |

---

### LegionPointsHistory

Paginated table of the authenticated user's points transactions (earned, redeemed, adjustments).

```tsx
import { LegionPointsHistory } from "@legionhandtech/lht-react-widgets";

<LegionPointsHistory
  baseUrl="https://your-legion-domain.com"
  authToken="your-jwt-token"
  theme="light"
  height="600px"
  pointsHistoryHeaderBg="#111"
  pointsHistoryHeaderTextColor="#fff"
  pointsHistoryAltRowBg="#f7f7f7"
/>;
```

| Prop        | Type                | Default      | Description                                                 |
| ----------- | ------------------- | ------------ | ----------------------------------------------------------- |
| `baseUrl`   | `string`            | **required** | Base URL of your Legion instance                            |
| `authToken` | `string`            | —            | JWT (relayed via `postMessage`, never as URL)               |
| `theme`     | `"light" \| "dark"` | `"light"`    | Preset theme                                                |
| `showClose` | `boolean`           | `false`      | Show a close button that posts `CLOSE_WIDGET` to the parent |
| `width`     | `string`            | `"100%"`     | Container width                                             |
| `height`    | `string`            | `"600px"`    | Container height                                            |

Theming props (all optional): `pointsHistoryHeaderBg`, `pointsHistoryHeaderTextColor`, `pointsHistoryAltRowBg`, `pointsHistoryTextColor`, `pointsHistoryBorderColor`, `pointsHistoryPaginationActiveBg`, `pointsHistoryPaginationActiveColor`, `pointsHistoryFontFamily`, `pointsHistoryTitleSize`, `pointsHistoryCellSize`.

---

## Common Props

Every widget also accepts:

| Prop        | Type                        | Description                                |
| ----------- | --------------------------- | ------------------------------------------ |
| `className` | `string`                    | CSS class on the wrapper `<div>`           |
| `style`     | `React.CSSProperties`       | Inline styles on the wrapper `<div>`       |
| `onLoad`    | `(detail) => void`          | Fires when the widget iframe loads         |
| `onError`   | `(message: string) => void` | Fires when the widget iframe fails to load |

## Theming

Each widget supports fine-grained CSS-variable theming via additional props. Pass them alongside the core props:

```tsx
<LegionOpportunities
  baseUrl="https://your-legion-domain.com"
  oppTitleColor="#1a1b1e"
  oppButtonBg="#228be6"
  oppCardRadius="16px"
  oppTransitionDuration="0.2s"
/>
```

Theming props are prefixed by widget name (`opp*`, `gallery*`, `qa*`, `social*`, `wallet*`, `matrix*`, `profile*`, `profileEdit*`, `notif*`, `saved*`, `groups*`, `header*`, `posts*`, `sidebar*`, `pointsHistory*`). The Campaigns Carousel uses camelCase props (`panelStart`, `panelMid`, etc.). The Pioneer Point Status widget has no theming props (it inherits the host page's styles). Full lists are available in each widget's TypeScript types:

```ts
import type {
  OpportunitiesThemingProps,
  CommunityGalleryThemingProps,
  QAThemingProps,
  SocialThemingProps,
  WalletThemingProps,
  MatrixThemingProps,
  ProfileThemingProps,
  ProfileEditThemingProps,
  NotificationsThemingProps,
  SavedOpportunitiesThemingProps,
  CampaignsCarouselThemingProps,
  CommunityGroupsThemingProps,
  CommunityHeaderThemingProps,
  CommunityPostsThemingProps,
  CommunitySidebarThemingProps,
  PointsHistoryThemingProps,
} from "@legionhandtech/lht-react-widgets";
```

## Imperative Ref

All widgets expose a `refresh()` method via React ref:

```tsx
import { useRef } from "react";
import { LegionOpportunities } from "@legionhandtech/lht-react-widgets";
import type { LegionOpportunitiesHandle } from "@legionhandtech/lht-react-widgets";

function App() {
  const ref = useRef<LegionOpportunitiesHandle>(null);

  return (
    <>
      <button onClick={() => ref.current?.refresh()}>Reload</button>
      <LegionOpportunities ref={ref} baseUrl="https://your-legion-domain.com" />
    </>
  );
}
```

Handle types: `LegionOpportunitiesHandle`, `LegionCommunityGalleryHandle`, `LegionQAHandle`, `LegionSocialHandle`, `LegionWalletHandle`, `LegionMatrixHandle`, `LegionProfileHandle`, `LegionProfileEditHandle`, `LegionNotificationsHandle`, `LegionSavedOpportunitiesHandle`, `LegionCampaignsCarouselHandle`, `LegionCommunityGroupsHandle`, `LegionCommunityHeaderHandle`, `LegionCommunityPostsHandle`, `LegionCommunitySidebarHandle`, `LegionPioneerPointStatusHandle`, `LegionPointsHistoryHandle`.

## Web Components (from NPM)

If you prefer custom elements over React components, import the web component entry:

```ts
// Register <legion-opportunities> custom element
import "@legionhandtech/lht-react-widgets/web-component";
```

Then use it in HTML or JSX:

```html
<legion-opportunities
  base-url="https://your-legion-domain.com"
  count="5"
  layout="horizontal"
  theme="light"
>
</legion-opportunities>
```

Available web component imports:

| Import Path                           | Custom Element                  |
| ------------------------------------- | ------------------------------- |
| `/web-component`                      | `<legion-opportunities>`        |
| `/community-gallery-web-component`    | `<legion-community-gallery>`    |
| `/qa-web-component`                   | `<legion-qa>`                   |
| `/social-web-component`               | `<legion-social>`               |
| `/wallet-web-component`               | `<legion-wallet>`               |
| `/matrix-web-component`               | `<legion-matrix>`               |
| `/profile-web-component`              | `<legion-profile>`              |
| `/profile-edit-web-component`         | `<legion-profile-edit>`         |
| `/notifications-web-component`        | `<legion-notifications>`        |
| `/saved-opportunities-web-component`  | `<legion-saved-opportunities>`  |
| `/campaigns-carousel-web-component`   | `<legion-campaigns-carousel>`   |
| `/community-groups-web-component`     | `<legion-community-groups>`     |
| `/community-header-web-component`     | `<legion-community-header>`     |
| `/community-posts-web-component`      | `<legion-community-posts>`      |
| `/community-sidebar-web-component`    | `<legion-community-sidebar>`    |
| `/pioneer-point-status-web-component` | `<legion-pioneer-point-status>` |
| `/points-history-web-component`       | `<legion-points-history>`       |

> Web component imports do **not** require React.

## Sub-path Imports

For smaller bundles, import only the widget you need:

```ts
import { LegionOpportunities } from "@legionhandtech/lht-react-widgets/opportunities";
import { LegionCommunityGallery } from "@legionhandtech/lht-react-widgets/community-gallery";
import { LegionQA } from "@legionhandtech/lht-react-widgets/qa";
import { LegionSocial } from "@legionhandtech/lht-react-widgets/social";
import { LegionWallet } from "@legionhandtech/lht-react-widgets/wallet";
import { LegionMatrix } from "@legionhandtech/lht-react-widgets/matrix";
import { LegionProfile } from "@legionhandtech/lht-react-widgets/profile";
import { LegionProfileEdit } from "@legionhandtech/lht-react-widgets/profile-edit";
import { LegionNotifications } from "@legionhandtech/lht-react-widgets/notifications";
import { LegionSavedOpportunities } from "@legionhandtech/lht-react-widgets/saved-opportunities";
import { LegionCampaignsCarousel } from "@legionhandtech/lht-react-widgets/campaigns-carousel";
import { LegionCommunityGroups } from "@legionhandtech/lht-react-widgets/community-groups";
import { LegionCommunityHeader } from "@legionhandtech/lht-react-widgets/community-header";
import { LegionCommunityPosts } from "@legionhandtech/lht-react-widgets/community-posts";
import { LegionCommunitySidebar } from "@legionhandtech/lht-react-widgets/community-sidebar";
import { LegionPioneerPointStatus } from "@legionhandtech/lht-react-widgets/pioneer-point-status";
import { LegionPointsHistory } from "@legionhandtech/lht-react-widgets/points-history";
```

## Host-level Campaign Detail Overlay

By default, clicking a card inside the embedded opportunities iframe opens the campaign-detail modal **within the iframe** — that is, it is clipped to the iframe's own bounding box, which is rarely the full viewport. The widget ships a host-level overlay mechanism that lifts the detail view out of the iframe and into the host page, rendering it at full-viewport size.

### How it works

When the user clicks a card, the widget iframe posts a message to the host page:

```
{ type: "LEGION_REQUEST_CAMPAIGN_DETAIL", id: "<campaignId>" }
```

The loader script (or NPM web-component / React component) listens for this message. When received from a known opportunities iframe, it:

1. Mounts a fixed-position, full-viewport overlay `<div>` on `document.body`.
2. Creates an `<iframe>` inside that overlay loading `/widget/campaign-detail/<id>?theme=…`.
3. Forwards the auth token via `CONFIG` + `AUTH_TOKEN` postMessages on iframe load.
4. ACKs back to the requesting iframe: `{ type: "LEGION_CAMPAIGN_DETAIL_ACCEPTED", id }`.
5. The requesting iframe receives the ACK and skips its local modal. If no ACK arrives within 200 ms (e.g. older loader version) the local modal opens as a fallback — no breakage on existing embeds.
6. The overlay is dismissed on ESC, backdrop click, or a `CAMPAIGN_DETAIL_CLOSE` message from the detail iframe.

### CustomEvents emitted on the host element

After the overlay manager handles a lifecycle message from the detail iframe, it re-emits a `CustomEvent` on the originating `<legion-opportunities>` element (or the React component's container `<div>`). All events bubble and are composed.

| Event name                     | Fired when                                                                       | `event.detail`           |
| ------------------------------ | -------------------------------------------------------------------------------- | ------------------------ |
| `campaign-detail-close`        | User dismisses the detail overlay (ESC, backdrop, or close button inside detail) | `{ id: string }`         |
| `campaign-detail-claim`        | User successfully completes the claim flow inside the detail overlay             | `{ id: string \| null }` |
| `campaign-detail-requestlogin` | The detail view requests the host to open a login prompt                         | `{ id: string \| null }` |

#### React example

```tsx
<LegionOpportunities
  baseUrl="https://your-legion-domain.com"
  ref={widgetRef}
  onLoad={...}
/>

// The container <div> wrapping the iframe is the event target.
// Attach listeners via ref or an ancestor element:
document.addEventListener("campaign-detail-close", (e) => {
  console.log("overlay closed for", (e as CustomEvent).detail.id);
});
document.addEventListener("campaign-detail-claim", (e) => {
  console.log("claimed", (e as CustomEvent).detail.id);
});
document.addEventListener("campaign-detail-requestlogin", () => {
  openYourLoginModal();
});
```

#### Web component / vanilla-JS example

```html
<legion-opportunities
  id="opp-widget"
  base-url="https://your-legion-domain.com"
  auth-token="<jwt>"
></legion-opportunities>

<script>
  const widget = document.getElementById("opp-widget");
  widget.addEventListener("campaign-detail-close", (e) => {
    console.log("overlay closed for", e.detail.id);
  });
  widget.addEventListener("campaign-detail-claim", (e) => {
    console.log("claimed", e.detail.id);
  });
  widget.addEventListener("campaign-detail-requestlogin", (e) => {
    openYourLoginModal();
  });
</script>
```

### postMessage contract (advanced / custom integrations)

If you build a completely custom host integration you can implement the overlay protocol directly:

| Direction            | Message type                      | Payload                 |
| -------------------- | --------------------------------- | ----------------------- |
| iframe → host        | `LEGION_REQUEST_CAMPAIGN_DETAIL`  | `{ type, id: string }`  |
| host → iframe        | `LEGION_CAMPAIGN_DETAIL_ACCEPTED` | `{ type, id: string }`  |
| detail iframe → host | `CAMPAIGN_DETAIL_CLOSE`           | `{ type }`              |
| detail iframe → host | `CAMPAIGN_DETAIL_CLAIMED`         | `{ type, id: string }`  |
| detail iframe → host | `CAMPAIGN_DETAIL_REQUEST_LOGIN`   | `{ type, id?: string }` |

The widget iframe posts `LEGION_REQUEST_CAMPAIGN_DETAIL` and waits **200 ms** for `LEGION_CAMPAIGN_DETAIL_ACCEPTED`. If the ACK is not received, it falls back to opening the in-iframe modal. This means you can opt out of the overlay by simply not replying to the request.

## TypeScript

The package ships with full TypeScript declarations (`.d.ts` and `.d.mts`). All prop interfaces, theming types, and ref handle types are exported.

## License

MIT
