npm package: @talarion/components
Description: React components for Talarion prediction market UIs
Latest version: 0.1.0

# @talarion/components

React components for [Talarion](https://talarion.tech) prediction market UIs. Ships a self-contained MarketCard with CSS-variable theming — no Tailwind required.

## Installation

```bash
pnpm add @talarion/components react
```

## Quick Start

```tsx
import { MarketCard, MarketGrid } from "@talarion/components";
import type { Market } from "@talarion/components";
import "@talarion/components/theme.css";

const markets: Market[] = [
  {
    id: "btc-100k",
    title: "Will BTC exceed $100K by end of 2026?",
    volume: 1_500_000,
    liquidity: 250_000,
    outcomes: [
      { label: "Yes", price: 0.65 },
      { label: "No", price: 0.35 },
    ],
  },
];

function App() {
  return (
    <MarketGrid expectedCount={markets.length}>
      {markets.map((m) => (
        <MarketCard
          key={m.id}
          market={m}
          onMarketClick={({ market }) => console.log("clicked", market.id)}
          onOutcomeClick={({ market, outcome }) =>
            console.log(`Trade ${outcome.label} on ${market.title}`)
          }
        />
      ))}
    </MarketGrid>
  );
}
```

## Components

### `MarketCard`

The primary component. Displays a market title, volume/liquidity/outcome stats, and Yes/No outcome buttons.

| Prop | Type | Description |
|------|------|-------------|
| `market` | `Market` | Market data to display |
| `disabled` | `boolean?` | Disable card and buttons |
| `onMarketClick` | `(event: MarketClickEvent) => void` | Card body clicked |
| `onOutcomeClick` | `(event: OutcomeClickEvent) => void` | Outcome button clicked |
| `className` | `string?` | Additional CSS class |

### `MarketGrid`

Responsive grid container. Uses `ResizeObserver` to compute column count from available width.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `children` | `ReactNode` | — | Card elements |
| `minCardWidth` | `number?` | `350` | Min card width (px) before columns collapse |
| `gap` | `number?` | `16` | Gap between cards (px) |
| `expectedCount` | `number?` | — | Limits max columns |
| `className` | `string?` | — | Additional CSS class |

### `StatDisplay`

Label + value stat pair with optional highlight color.

### `OutcomeButtons`

Yes/No pill buttons with price in cents. Used internally by MarketCard but exported for standalone use.

### `MarketRulesModal`

Full-screen overlay modal showing settlement rules and resolution date for a market. Closes on backdrop click or the close button.

| Prop | Type | Description |
|------|------|-------------|
| `isOpen` | `boolean` | Whether the modal is visible |
| `onClose` | `() => void` | Called when the modal should close |
| `title` | `string` | Market title shown in the header |
| `rules` | `string?` | Settlement / description rules text |
| `expirationDate` | `string?` | Expected resolution date (ISO string) |

### `TalarionLogo`

Talarion "T" logo in a circular badge. White by default, clickable (links to talarion.tech).

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `size` | `number?` | `24` | Circle diameter in px |
| `clickable` | `boolean?` | `true` | Render as link to talarion.tech |
| `className` | `string?` | — | Additional CSS class |

## Integrating MarketCard with MarketRulesModal

The modal is a separate component — wire it up via `onMarketClick`:

```tsx
import { useState } from "react";
import { MarketCard, MarketRulesModal, MarketGrid } from "@talarion/components";
import type { Market, MarketClickEvent } from "@talarion/components";
import "@talarion/components/theme.css";

const markets: Market[] = [/* your markets */];

// Extend with rules data your app has available
const rulesMap: Record<string, { rules: string; expirationDate: string }> = {
  "btc-100k": {
    rules: "Resolves YES if BTC exceeds $100,000 on any major exchange before expiration.",
    expirationDate: "2026-12-31",
  },
};

function App() {
  const [selected, setSelected] = useState<Market | null>(null);

  return (
    <>
      <MarketGrid expectedCount={markets.length}>
        {markets.map((m) => (
          <MarketCard
            key={m.id}
            market={m}
            onMarketClick={({ market }) => setSelected(market)}
            onOutcomeClick={({ outcome }) => console.log(outcome.label)}
          />
        ))}
      </MarketGrid>

      {selected && (
        <MarketRulesModal
          isOpen={!!selected}
          onClose={() => setSelected(null)}
          title={selected.title}
          rules={rulesMap[selected.id]?.rules}
          expirationDate={rulesMap[selected.id]?.expirationDate}
        />
      )}
    </>
  );
}
```

## Types

```ts
interface Market {
  id: string;
  title: string;
  volume: number | null;
  liquidity: number | null;
  outcomes: MarketOutcome[];
}

interface MarketOutcome {
  label: string;   // "Yes", "No", "Trump", etc.
  price: number;   // 0–1 decimal (0.65 = 65¢)
}

interface MarketClickEvent { market: Market }
interface OutcomeClickEvent { market: Market; outcome: MarketOutcome; outcomeIndex: number }
```

## Theming

The default theme is dark. Override any `--tlr-*` CSS variable to customize:

```css
:root {
  --tlr-card-bg: #ffffff;
  --tlr-card-bg-hover: #f5f5f5;
  --tlr-text-primary: #111111;
  --tlr-text-tertiary: #777777;
  --tlr-success-bg: #f4f7f4;
  --tlr-success-text: #4d6b4d;
  --tlr-danger-bg: #f8f4f4;
  --tlr-danger-text: #8a5555;
  /* ... see theme.css for all variables */
}
```

All components carry `data-tlr` attributes (`data-tlr="card"`, `data-tlr="grid"`) for targeted CSS overrides.

### Available Variables

| Variable | Description |
|----------|-------------|
| `--tlr-card-bg` | Card background |
| `--tlr-card-bg-hover` | Card hover background |
| `--tlr-card-border` | Card border color |
| `--tlr-card-radius` | Card border radius |
| `--tlr-card-padding` | Card padding |
| `--tlr-card-shadow` | Card box shadow |
| `--tlr-card-shadow-hover` | Card hover shadow |
| `--tlr-text-primary` | Primary text color |
| `--tlr-text-tertiary` | Tertiary/label text color |
| `--tlr-success-bg` | Yes button background |
| `--tlr-success-text` | Yes button text |
| `--tlr-success-border` | Yes button border |
| `--tlr-danger-bg` | No button background |
| `--tlr-danger-text` | No button text |
| `--tlr-danger-border` | No button border |
| `--tlr-btn-radius` | Button border radius |
| `--tlr-btn-min-width` | Button minimum width |
| `--tlr-font-family` | Font family |
| `--tlr-logo-color` | Logo fill color |

## Utilities

Exported for convenience:

- `formatVolume(usd: number)` — `$1.50M`, `$150.0K`, `$500`
- `formatLiquidity(usd: number)` — `$1.50M`, `$150K`, `$500`
- `convertPriceToCents(price: number)` — `0.65` → `"65.0"`

## Docs

Full Talarion documentation at **[docs.talarion.tech](https://docs.talarion.tech)**.

## License

MIT