<p align="center">
  <img src="docs/banner.svg" alt="News Factory — widgets-react-components" width="100%">
</p>

<p align="center">
  <img alt="npm" src="https://img.shields.io/badge/npm-%40news--factory%2Fwidgets--react--components-CB3837?style=flat-square&logo=npm&logoColor=white">
  <img alt="React" src="https://img.shields.io/badge/React-peer_dep-087EA4?style=flat-square&logo=react&logoColor=white">
  <img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-5-3178C6?style=flat-square&logo=typescript&logoColor=white">
  <img alt="Build" src="https://img.shields.io/badge/build-tsup_(cjs%2Besm%2Bdts)-FDBA74?style=flat-square">
  <img alt="Locales" src="https://img.shields.io/badge/locales-24-36D95A?style=flat-square">
</p>

---

## What this is

Every widget a News Factory client embeds on their own site, published as one npm package:
`@news-factory/widgets-react-components`.

Each widget is fully **white-label** — colours, fonts, spacing and layout come from a per-client
style record, so the same component renders as a dozen different brands. Text is translated into
24 locales; only the *data* (company names, numbers, dates) stays in English.

## Where it sits

```
  staff pick colours + layout          the customer pastes one <iframe>
  in the backoffice editor                        │
             │                                    ▼
             ▼                            video_iframe (Next.js)
   backoffice.WDGT_styles                         │
   └── style_payload  (JSON) ─────────────────────┤ renders
                                                  ▼
                              @news-factory/widgets-react-components
                                                  │ fetches its own data
                                                  ▼
                                   /api/widgets/*  (candles, movers, …)
```

The package is consumed by **video_iframe** (which serves the customer iframe) and by
**backoffice-rewrite** (which previews it live in the style editor).

## The widgets

| Group | Components |
| --- | --- |
| **News** | `News` (v1, legacy), `News2` (editorial), `News3` (dashboard), `News4` |
| **Market data** | `Heatmap`, `TopMovers`, `Signals`, `AnalystRecommendations`, `TransactionAnalysis` |
| **Calendar** | `EconomicCalendar` — plus the Hola Prime variant with Forex / Futures / Stocks tabs |
| **Calculators** | `CurrencyConverter`, `MarginCalculator`, `Fibonacci`, `Pivots`, `StopLossTakeProfit`, `StopLossTakeProfitLevels` |
| **Charts** | `StockWidget` (price chart, 15M / 1H / 1D), `AreaChart` |
| **Other** | `Glossary`, `academy`, `combos`, `special` |

News v1 / v2 / v3 are chosen **server-side** from `WDGT_styles.news_version`. The customer's embed
URL never changes — staff flip the column and the iframe picks it up on the next refresh. A client
stays on the old design simply by leaving that column alone.

## Install & use

```bash
npm i @news-factory/widgets-react-components
```

```tsx
import { Heatmap } from '@news-factory/widgets-react-components';
```

Two entry points:

| Import path | Contents | Safe in a server component? |
| --- | --- | --- |
| `@news-factory/widgets-react-components` | All widgets (client components) | **no** |
| `@news-factory/widgets-react-components/payload` | `toPayload`, `WIDGET_STYLE_SCHEMA_VERSION`, the field partition | yes — React-free |

> **Server code must import from `/payload`, never the barrel.** The barrel re-exports client
> widgets that use `useRef` / `useState`, and Next throws *"importing a module that depends on
> useRef into a Server Component"*.

## Development

```bash
npm run build              # tsup — BOTH entry points, cjs + esm + dts
npm run dev                # same, watch mode
npm run push-and-publish   # commit, bump, publish, push (publish.js)
```

> `publish.js` must build via `npm run build`. A hardcoded single-entry `tsup src/index.ts` drops
> the `/payload` subpath and breaks every server-side import in production.

### Local development against a consumer app

Both consumers symlink a local build; **neither hack is ever committed**.

| Consumer | Needs |
| --- | --- |
| `video_iframe` (webpack) | `transpilePackages` in `next.config` |
| `backoffice-rewrite` (Next 16 + Turbopack) | `transpilePackages` **plus** `turbopack.root` and `outputFileTracingRoot` set to `path.join(__dirname, "..")`, or it fails to resolve the package |

`npm publish` is the only way a change reaches production.

## Styling

The ~111 design fields live in a single `style_payload` JSON column on `backoffice.WDGT_styles`,
gated by `style_format`. The **field partition is the source of truth** in
[src/types/WidgetStylePayload.ts](src/types/WidgetStylePayload.ts) — `PAYLOAD_FIELDS` is a manual
allowlist (`as const satisfies`, **not** `Omit`) with a compile-time exhaustiveness guard.

Identity (`id`, `brand_id`, …) and routing (`news_version`, `news_version_staged`) stay **columnar**
and must never enter the payload.

## Gotchas

- **Never touch `window` or `document` at module-eval in anything the barrel re-exports.** The package is imported by Next server components; a static `import WebFont from "webfontloader"` once broke SSR of every page that imported the barrel. Use a lazy `import()` inside `useEffect`.
- **Fetch functions must use the absolute `https://widget.newsfactory.tv/...` URL.** The widget runs inside a customer-embedded iframe — a relative URL hits *their* origin. Keep the commented `localhost:3001` line as the local toggle.
- **A new news category is a change here too.** `CATEGORY_LABELS`, `CATEGORY_DEFAULT_COLORS`, `CATEGORY_CHART_SYMBOL`, `CATEGORY_ASSETS` and the dictionaries all hard-code the public categories. An unknown `item_category` fails **silently** — the chip falls back to an unrelated label and the chart vanishes. Private categories go in `PRIVATE_NEWS_CATEGORIES`, never in `NEWS_CATEGORIES`.
- **A table the reader UNIONs must be created `COLLATE=utf8mb4_unicode_ci`.** The schema default is `utf8mb4_0900_ai_ci`, and a mixed-collation UNION fails the *whole* query — the entire tab renders empty, not just the new rows.
- **Cross-origin TradingView iframes cannot be styled from inside.** Logos are hidden with masking divs, the TA gauge is shrunk with `transform: scale()` (not container height, which clips), and stray clicks are blocked with an overlay.

## Related repos

| Repo | Role |
| --- | --- |
| `video_iframe` | Serves the customer iframe and the `/api/widgets/*` data endpoints |
| `backoffice-rewrite` | The style editor and live preview; owns `WDGT_styles` |
| `economic-calendar-api` | Exposes the same widget data to clients as authed REST |
| `nf-aws-infrastructure` | `topMoversCron`, candle and calendar Lambdas fill the tables these widgets read |

---

<p align="center">
  <sub>News Factory · internal — not for distribution</sub>
</p>
