npm package: @iccandle/selector
Description: React scanner overlay for TradingView Charting Library — ICCandle pattern search, remote theming, and plugin iframe integration.
Author: ICCandle
Latest version: 0.0.43

# @iccandle/selector

React component that wraps an existing [TradingView Charting Library](https://www.tradingview.com/charting-library-docs/) widget and adds ICCandle’s **scanner UI**: a draggable popup to run pattern search over a user-selected bar range, with theming loaded from ICCandle’s API.

Published as ESM and CommonJS; component styles are bundled and injected at runtime (no separate CSS import).

## Install

```bash
npm install @iccandle/selector
```

### How to get an API key

1. Register or sign in at the [ICCandle corporate portal](https://corporate-iccandle.vercel.app/).
2. Use **Create API key** in the dashboard.
3. When selecting a service, choose **search** so the key is valid for this widget (remote theming, candle cache, and scanner validation).

Issued keys use the format `icc_search_` followed by 48 hexadecimal characters.

### Prerequisites

- **React 18+** — `react` and `react-dom` are **peer dependencies** (install them in your app).
- **TradingView Charting Library** — obtain it under your own license from TradingView, host the static assets (e.g. under `/charting_library/` in your public folder), and load the library at runtime. **This package does not ship the charting library.**
- **ICCandle API key** — required for theme, candle cache, and scanner validation; format `icc_search_` plus 48 hex characters. See [How to get an API key](#how-to-get-an-api-key) above.

## Quick start

```tsx
import { useState } from "react";
import type { IChartingLibraryWidget } from "charting_library/charting_library";
import { SelectorWidget } from "@iccandle/selector";

function App() {
  const [chartWidget, setChartWidget] = useState<IChartingLibraryWidget | null>(
    null,
  );
  const widgetKey = "icc_search_..."; // ICCandle-issued key (48 hex chars after prefix)

  return (
    <SelectorWidget
      chartWidget={chartWidget}
      widgetKey={widgetKey}
      theme="system"
      submitCallback={(iframeSrc) => {
        // Full URL for the ICCandle plugin iframe (open in modal, new window, etc.)
        console.log(iframeSrc);
      }}
    >
      {/* Your chart container + TradingView bootstrap; call setChartWidget when ready */}
      <div id="tv_chart_container" style={{ height: "100%" }} />
    </SelectorWidget>
  );
}
```

Replace the chart placeholder with your TradingView initialization and pass the `IChartingLibraryWidget` instance when `onChartReady` (or equivalent) fires.

## Usage guide

### Step 1 — Install the package

```bash
npm install @iccandle/selector
```

Ensure `react` and `react-dom` are installed and meet the peer version range.

### Step 2 — Host the Charting Library

1. Copy the TradingView Charting Library build into a path your app can serve as static files (e.g. `public/charting_library/` in Vite or Create React App).
2. Import the library constructor from that path in your bundler setup (see TradingView’s integration docs for your framework). The library is **not** bundled inside `@iccandle/selector`; it loads at runtime via `library_path` (or equivalent) on the widget options.

### Step 3 — Bootstrap TradingView and capture the widget instance

Create a ref for the chart DOM node, instantiate the widget in `useEffect`, store the instance in React state, and remove it on cleanup:

```tsx
import { useEffect, useRef, useState } from "react";
import type {
  ChartingLibraryWidgetOptions,
  IChartingLibraryWidget,
  ResolutionString,
} from "charting_library/charting_library";
import { widget } from "charting_library/charting_library";

const LIBRARY_PATH = "/charting_library/"; // must match your hosted assets

// Inside your component:
const containerRef = useRef<HTMLDivElement>(null);
const [chartWidget, setChartWidget] = useState<IChartingLibraryWidget | null>(
  null,
);

useEffect(() => {
  const el = containerRef.current;
  if (!el) return;

  const options: ChartingLibraryWidgetOptions = {
    container: el,
    library_path: LIBRARY_PATH,
    symbol: "EURUSD",
    interval: "60" as ResolutionString,
    datafeed: yourDatafeed,
    locale: "en",
    autosize: true,
  };

  const tv = new widget(options);
  setChartWidget(tv);

  return () => {
    try {
      tv.remove();
    } catch {
      /* no-op */
    }
    setChartWidget(null);
  };
}, [/* library_path, datafeed identity, or other inputs that should recreate the chart */]);
```

You must supply a valid `datafeed`, `symbol`, `interval`, `locale`, and any other options required by your TradingView license and app. Import paths for `widget` and types differ by setup (`charting_library/charting_library`, `./public/charting_library`, etc.—follow TradingView’s docs for your bundler). If your integration only exposes the instance after `onChartReady`, call `setChartWidget` inside that callback instead of immediately after `new widget(...)`.

### Step 4 — Wrap the chart with `SelectorWidget`

`SelectorWidget` must wrap the same subtree that contains the chart container so the scanner overlay positions correctly. Pass the live widget instance (or `null` while mounting):

```tsx
import { SelectorWidget } from "@iccandle/selector";

const widgetKey = "icc_search_..."; // your ICCandle key

<SelectorWidget
  chartWidget={chartWidget}
  widgetKey={widgetKey}
  theme="system"
  submitCallback={(iframeSrc) => {
    /* see Step 5 */
  }}
>
  <div ref={containerRef} style={{ height: "100%", minHeight: 400 }} />
</SelectorWidget>
```

### Step 5 — Handle the plugin iframe URL

After a successful scan setup, `submitCallback` receives a **full HTTPS URL** for the ICCandle plugin iframe. The query string typically includes the bar window (timestamps / size), symbol, `candle_id`, `apiKey` (your widget key), resolved theme, and any active filters—use it as-is in an iframe `src` or deep link.

**Open in a new tab**

```ts
submitCallback={(iframeSrc) => {
  window.open(iframeSrc, "_blank", "noopener,noreferrer");
}}
```

**Show in a modal or side panel**

Store the URL in state and render:

```tsx
{iframeSrc ? (
  <iframe title="ICCandle pattern search" src={iframeSrc} className="..." />
) : null}
```

**TypeScript:** import `SelectorWidgetProps` if you wrap `SelectorWidget` in your own component and want explicit prop typing. Import `IChartingLibraryWidget` from **your** Charting Library typings path (`charting_library/charting_library` or the path your project uses)—this package does not re-export TradingView types.

### Theme and remote branding

- **`theme="light"` / `"dark"`** — forces that palette for the scanner chrome and for values forwarded into the plugin URL.
- **`theme="system"`** (recommended when you don’t control parent theme) — follows `prefers-color-scheme` for light/dark resolution.
- On mount, the widget fetches your org’s tokens from ICCandle (see [Widget key and remote theming](#widget-key-and-remote-theming)) and sets CSS custom properties on the widget root, e.g. `--iccandle-primary`, `--iccandle-border`, so the in-chart scanner matches your configured light/dark branding. The same resolved theme is reflected in the iframe URL so the plugin UI stays consistent.

### Full working example

Minimal end-to-end pattern: chart container ref, widget lifecycle, `SelectorWidget`, and iframe after submit. Replace `yourDatafeed` and widget options with your real datafeed and TradingView settings.

```tsx
import { useEffect, useRef, useState } from "react";
import type {
  ChartingLibraryWidgetOptions,
  IChartingLibraryWidget,
  ResolutionString,
} from "charting_library/charting_library";
import { widget } from "charting_library/charting_library";
import { SelectorWidget } from "@iccandle/selector";

const LIBRARY_PATH = "/charting_library/";
const WIDGET_KEY = "icc_search_your48hexcharactershere............";

export function ChartWithIccandleScanner() {
  const containerRef = useRef<HTMLDivElement>(null);
  const [chartWidget, setChartWidget] = useState<IChartingLibraryWidget | null>(
    null,
  );
  const [pluginSrc, setPluginSrc] = useState("");

  useEffect(() => {
    const el = containerRef.current;
    if (!el) return;

    const options: ChartingLibraryWidgetOptions = {
      container: el,
      library_path: LIBRARY_PATH,
      symbol: "EURUSD",
      interval: "60" as ResolutionString,
      datafeed: yourDatafeed,
      locale: "en",
      autosize: true,
      fullscreen: false,
      drawings_access: {
        type: "black",
        tools: [{ name: "Date Range" }],
      },
    };

    const tv = new widget(options);
    setChartWidget(tv);

    return () => {
      try {
        tv.remove();
      } catch {
        /* no-op */
      }
      setChartWidget(null);
    };
  }, []);

  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
      <div style={{ height: 500, width: "100%" }}>
        <SelectorWidget
          chartWidget={chartWidget}
          widgetKey={WIDGET_KEY}
          theme="system"
          submitCallback={(iframeSrc) => setPluginSrc(iframeSrc)}
        >
          <div ref={containerRef} style={{ height: "100%", width: "100%" }} />
        </SelectorWidget>
      </div>
      {pluginSrc ? (
        <iframe
          title="ICCandle search"
          src={pluginSrc}
          style={{ width: "100%", height: 600, border: "1px solid #ccc" }}
        />
      ) : null}
    </div>
  );
}
```

For a concrete in-repo reference (custom datafeed, timezone, visibility handling), see [`src/tradingview/TradingviewChart.tsx`](src/tradingview/TradingviewChart.tsx) in this repository’s dev app.

### Common patterns

| Pattern | Approach |
| -------- | -------- |
| **Modal iframe** | On `submitCallback`, set state and render `<iframe src={url} />` inside a `<dialog>`, Radix Dialog, MUI Modal, etc. |
| **New tab** | `window.open(iframeSrc, "_blank", "noopener,noreferrer")`. |
| **Split layout** | Keep the chart in one column and mount the iframe in another when `pluginSrc` is set (as in the example above). |
| **News / events on the time axis** | If your datafeed implements `getTimescaleMarks`, you can sync marks with `localStorage` — see [Optional: timescale marks (news/events)](#optional-timescale-marks-newsevents). |

### Troubleshooting

| Issue | What to check |
| ----- | --------------- |
| Chart area stays blank | `library_path` must point to the folder URL where TradingView static files are served; container ref must be mounted before `new widget(...)`. |
| `chartWidget` is always `null` | Ensure you call `setChartWidget` after the widget is created (or inside `onChartReady` if your integration requires it). |
| Scanner or theme looks wrong | Confirm `widgetKey` is valid; theme fetch uses `api-key: <widgetKey>`. Network or auth failures may leave defaults. |
| CORS / network errors on theme API | In local dev, browser calls to `api.iccandle.ai` may be blocked by CORS unless you proxy; the widget should still function with fallback styling—verify in production or behind your proxy. |
| Date range tool missing | Enable the Date Range drawing in `drawings_access` (see full example) so users can select the bar window the scanner uses. |

## API

### Exports

| Name                 | Kind   | Description                                  |
| -------------------- | ------ | -------------------------------------------- |
| `SelectorWidget`     | Component | Scanner overlay around your chart subtree |
| `SelectorWidgetProps`| Type   | Props for `SelectorWidget`                   |

### `SelectorWidget` props

| Prop              | Type                                    | Required | Description |
| ----------------- | --------------------------------------- | -------- | ----------- |
| `chartWidget`     | `IChartingLibraryWidget \| null`        | Yes      | Live TradingView widget instance (`null` until ready). |
| `children`        | `ReactNode`                             | Yes      | Your chart UI (e.g. container + library bootstrap). |
| `widgetKey`       | `string`                                | Yes      | ICCandle API key (`icc_search_` + 48 hex chars). Used for theme fetch, candle cache, and scanner validation. |
| `submitCallback`  | `(iframeSrc: string) => void`           | Yes      | Called after a successful scan setup with the **plugin iframe URL** (query string includes window size, timestamps, symbol, `candle_id`, `apiKey`, theme, optional filters). |
| `theme`           | `"light" \| "dark" \| "system"`         | No       | Defaults to sensible behavior; `"system"` follows `prefers-color-scheme`. Affects resolved theme tokens and the plugin URL. |

### Widget key and remote theming

On mount, the component loads branding colors from ICCandle:

- **URL:** `https://api.iccandle.ai/corporate-client/v1/widgetStyle/search/user/?service_type=search`
- **Header:** `api-key: <widgetKey>`

CSS custom properties (`--iccandle-primary`, `--iccandle-border`, etc.) are applied on the widget root so the scanner matches your configured light/dark tokens.

**Theme prop vs. API tokens:** The `theme` prop (`"light"` \| `"dark"` \| `"system"`) chooses which variant of those tokens to apply and is also forwarded in the plugin iframe URL. `"system"` tracks `prefers-color-scheme` so the scanner and plugin stay aligned with the user’s OS preference when you don’t pass an explicit light/dark mode from your app shell.

### Behavior summary

- Subscribes to chart readiness, resolution, symbol changes, and drawing events.
- Manages a `date_range` multipoint drawing so the user can adjust the bar window; window size and exported candles drive the scanner.
- Posts candles to ICCandle’s cache endpoint before opening the plugin URL (see scanner implementation for details).
- Listens for `window` `message` events for chart/news integration and can clear persisted news mark selections (`localStorage` key `tv:selected-news-events`) when starting a scan.

## Optional: timescale marks (news/events)

If your data feed implements `getTimescaleMarks`, you can surface stored events (e.g. from `localStorage` under `tv:selected-news-events`) as marks on the time axis. Example shape:

```ts
getTimescaleMarks: async (
  symbolInfo: LibrarySymbolInfo,
  from: number,
  to: number,
  onResult: GetMarksCallback<TimescaleMark>,
) => {
  const marks: TimescaleMark[] = [];
  try {
    const allNewsEvents = JSON.parse(
      localStorage.getItem("tv:selected-news-events") || "[]",
    ) as NewsEventType[];

    allNewsEvents?.forEach(({ id, timestamp, event_name, currency }) => {
      if (!id || !Number.isFinite(timestamp) || timestamp <= 0) return;
      if (marks.some((m) => String(m.id) === String(id))) return;

      marks.push({
        id,
        time: timestamp / 1000,
        color: "green",
        label: event_name.slice(0, 1) || "N",
        tooltip: [event_name],
        ...(currency ? { imageUrl: `/images/symbols/${currency}.svg` } : {}),
        showLabelWhenImageLoaded: true,
      });
    });
  } catch {
    /* ignore */
  }
  onResult(marks);
};
```

Adjust paths and types to match your app and TradingView typings.

## Development (this repo)

| Script        | Command      | Purpose |
| ------------- | ------------ | ------- |
| Dev demo      | `npm run dev` | Vite app with local charting library (see `vite.config.ts`). |
| Library build | `npm run build` | Emits `dist/` (JS, CJS, bundled CSS injection, declarations). |
| Lint          | `npm run lint` | ESLint. |

`prepublishOnly` runs `build` before publish.

## License

MIT. TradingView Charting Library is subject to its own license from TradingView.