npm package: @oliv-e/vitals
Description: Componentes React do Oliv-e Vitals
Latest version: 0.4.1

# @oliv-e/vitals

Biblioteca de componentes React e widget embutível (Custom Element) para iniciar a medição de sinais vitais via iFrame. Este projeto entrega:

- Lib npm para React/TypeScript (ESM + CJS + tipos .d.ts)
- Bundle standalone para integração via SCRIPT TAG (<vitals-widget />)


## Sumário

- Visão Geral
- Estrutura do projeto
- Uso como biblioteca React (npm)
- Uso via SCRIPT TAG (widget standalone)
- Atributos (obrigatórios e opcionais)
- Build, testes e publicação
- Tipos exportados
- Eventos e comunicação
- Configuração de estilos (Tailwind/CSS)
- Dicas de troubleshooting


## Visão Geral

- A lib React expõe os componentes que constroem um iFrame apontando por padrão para `https://vitals.oliv-e.health/widget/` e fazem o handshake com `postMessage + MessageChannel` para receber resultados. Essa URL pode ser customizada via `WIDGET_BASE_URL` (ver abaixo), mantendo o mesmo contrato.
- Eventos padronizados propagados para `window`: `vh:handshake`, `vh:onready`, `vh:result`, `vh:onsuccess`, `vh:onerror`, `vh:onevent`.
- O bundle standalone disponibiliza o Custom Element `<vitals-widget>` para integrar via SCRIPT TAG sem React.


## Estrutura do projeto

```
.
├── dist/                      # Artefatos de build (lib + widget standalone)
├── src/
│   ├── index.ts               # Entry da lib (reexporta componentes e tipos)
│   └── widget/
│       ├── components/
│       │   ├── widget.tsx             # Componente que renderiza o iFrame e o handshake
│       │   └── widget-container.tsx   # Provider + composição do widget
│       │   └── widget-sdk.tsx         # Provider + composição do widget para aplicações React
│       ├── lib/
│       │   ├── context.ts             # Contexto do widget
│       │   ├── types.ts               # Tipos públicos exportados
│       │   └── use-widget-context.ts  # Hook utilitário
│       ├── styles/
│       │   └── style.css              # Estilos do widget (Tailwind @apply)
│       ├── custom-element.tsx         # Definição do <vitals-widget /> (Shadow DOM)
│       └── index.tsx                  # Entry para o bundle standalone
├── test/
│   └── index.html              # Página de teste local do widget standalone
├── rollup.config.mjs           # Build do widget standalone (IIFE)
├── vite.config.lib.ts          # Build da lib (ESM/CJS + d.ts)
├── tailwind.config.cjs         # Configuração Tailwind para build do CSS
├── postcss.config.cjs          # PostCSS + autoprefixer
├── .env.development            # Variáveis para build:widget ambiente dev
├── .env.production             # Variáveis para build:widget ambiente prod
├── package.json
└── README.md
```


## Uso como biblioteca React (npm)

Instalação (após publicação):

```bash
pnpm add @oliv-e/vitals @tanstack/react-query
```

Nota: @tanstack/react-query é peerDependency do SDK.

Exemplo recomendado (default export):

```tsx
import Vitals from '@oliv-e/vitals';

export default function Page() {
  return (
    <Vitals
      clientKey="<CLIENT_KEY>"
      user="14267197016"   // OBRIGATÓRIO: CPF
      birthyear={1999}
      height={170}
      weight={65}
      sex={1}
      bp_group="primary"
      bp_mode="normal"
      facing_mode="user"
    />
  );
}
```

Notas:
- Obrigatório: clientKey (identificador público do parceiro) e user (CPF).
- Opcionais: birthyear, height, weight, sex, bp_group, bp_mode, facing_mode, scanOnly.
- sex: 0 = female, 1 = male.
- facing_mode: 'user' | 'environment' (padrão: 'user').
- scanOnly: quando true, o iFrame renderiza somente vídeo+canvas (sem botões/overlays) e delega o início (InitScan) e o redirecionamento de sucesso ao parent. O parent deve enviar o comando InitScan via MessageChannel e tratar o evento OnSuccess.

### Resolução do parceiro (SDK)
- O SDK busca os dados do parceiro via `GET https://api-dev.oliv-e.health/company/widget/themes/${clientKey}` com header `x-partner-code: <clientKey>`. Retornos 404 disparam o erro: `O parceiro informado não está registrado para utilização.`.
- Se retornar 404, será lançado o erro: `O parceiro informado não está registrado para utilização.`.

### Envio automático de sucesso (SDK)
- Ao receber o evento de sucesso do iFrame, o SDK envia um POST para `https://api-dev.oliv-e.health/clinical-metrics/partner/clinical-measurement/` com:
  - Header: `x-partner-code: <clientKey>` e `Content-Type: application/json`
  - Body: `{ ...dadosDoEvento, metadata: { Cpf: "<CPF do usuário>" } }`
- Falhas no POST não bloqueiam o fluxo; são logadas no console.

### React Query (TanStack) – opcional
- O SDK aceita um `queryClient` (prop) para compartilhar o cache com a aplicação host.
- Se não for fornecido, o SDK cria um `QueryClient` interno e envolve o widget em `QueryClientProvider`.
- Para evitar múltiplos caches e requisições duplicadas, recomenda-se passar o `QueryClient` da aplicação host.

Exemplos:

Usando QueryClient da aplicação:
```tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import Vitals from '@oliv-e/vitals';

const qc = new QueryClient();

export default function Page() {
  return (
    <QueryClientProvider client={qc}>
      <Vitals clientKey="<CLIENT_KEY>" user="14267197016" />
    </QueryClientProvider>
  );
}
```

Passando via prop:
```tsx
import { QueryClient } from '@tanstack/react-query';
import VitalsWidget from '@oliv-e/vitals-widget';

const qc = new QueryClient();

export default function Page() {
return <Vitals clientKey="<CLIENT_KEY>" user="14267197016" queryClient={qc} />;
}
```

Exemplo mínimo (clientKey + user):

```tsx
import VitalsWidget from '@oliv-e/vitals-widget';

export default function Page() {
return <Vitals clientKey="your_client_key" user="14267197016" />;
}
```

CSS
- O pacote exporta um CSS compilado (dist/vitals-widget.css). Na maioria dos bundlers, importar a lib já injeta o CSS. Caso seu bundler não carregue CSS de node_modules automaticamente, importe explicitamente:

```ts
import '@oliv-e/vitals/dist/vitals.css';
```

Uso avançado (opcional) com WidgetContainer
- Caso você já possua o trio (client, name, theme), é possível montar o widget diretamente:

```tsx
import { WidgetContainer } from '@oliv-e/vitals';

export default function Page() {
  return (
    <WidgetContainer
      client="..."
      name="..."
      theme="standard"
      birthyear={1999}
      height={170}
      weight={65}
      sex={1}
      bp_group="primary"
      bp_mode="normal"
      facing_mode="user"
    />
  );
}
```

Tipos exportados úteis:

```ts
import type {
  VitalsConfig,
  Sex,
  FacingMode,
  BPMode,
  BPGroup,
  WidgetEventMap,
  WidgetSDK as WidgetSDKProps
} from '@oliv-e/vitals-widget';
```

Observação: o componente React padrão exportado chama-se `Vitals` (default export).

### Provider e Hooks (novo em 0.3.0)

Para integrações com mais controle de layout/estado (como sobrepor UI própria, ex.: Tauri), você pode usar o Provider e os hooks exportados.

- VitalsProvider: resolve o parceiro (via clientKey), registra listeners e publica OnSuccess automaticamente; provê o WidgetContext para os filhos
- useVitalsContext: acessa a configuração resolvida (client, name, theme, user, etc.)
- useVitalsEvents: inscreve-se nos eventos do widget e expõe controles `initScan` e `stopScan`
- Widget: componente que renderiza somente o iFrame (sem Providers); útil dentro do Provider

Exemplo:

```tsx
import { VitalsProvider, Widget, useVitalsEvents } from '@oliv-e/vitals';
import '@oliv-e/vitals/dist/vitals.css';

function Overlay() {
  const { isReady, lastResult, initScan } = useVitalsEvents();
  return (
    <div className="absolute inset-0 pointer-events-none">
      <button className="pointer-events-auto" disabled={!isReady} onClick={initScan}>
        Iniciar medição
      </button>
      {/* Exemplo simples de progresso vindo de lastResult */}
      <pre>{JSON.stringify(lastResult)}</pre>
    </div>
  );
}

export default function Page() {
  return (
    <VitalsProvider clientKey="<CLIENT_KEY>" user="14267197016" scanOnly>
      <div className="relative">
        <Widget />
        <Overlay />
      </div>
    </VitalsProvider>
  );
}
```

Notas:
- `scanOnly` é recomendado quando você fornece sua própria UI (o iFrame renderiza apenas vídeo/canvas)
- Você também pode obter as funções via ref no `VitalsProvider` (mesmo contrato de `WidgetControls`)


## Uso via SCRIPT TAG (widget standalone)

Build do widget (dev/prod):

```bash
pnpm build:widget
pnpm build:widget:production
```

Servir e testar:

```bash
pnpm serve:widget   # serve ./dist na porta 3334
pnpm serve          # serve ./test na porta 3333
```

Abra `http://localhost:3333/test/index.html` — o HTML de teste carrega `http://localhost:3334/widget.js` e instância o Custom Element:

```html
<script async src="http://localhost:3334/widget.js"></script>
<vitals-widget
  client-key="<CLIENT_KEY>"
  user="14267197016"
  birthyear="1999"
  height="175"
  weight="55"
  sex="1"
  bp-group="primary"
  bp-mode="normal"
  facing-mode="user"
/>
```

Nota: obrigatórios: client-key e user (CPF). Opcionais: birthyear, height, weight, sex, bp-group, bp-mode, facing-mode (padrão: 'user').

Resolução do parceiro (Web Component)
- O Web Component utiliza um mapeamento local (clients.json). O `client-key` deve existir nesse arquivo para que o widget funcione.

Exemplo mínimo (client-key + user):

```html
<script async src="http://localhost:3334/widget.js"></script>
<vitals-widget client-key="your_client_key" user="14267197016"></vitals-widget>
```

Publicação em CDN (ex.: jsDelivr):

```html
<script async src="https://cdn.jsdelivr.net/npm/@oliv-e/vitals@<versao>/dist/widget.js"></script>
```

Nota: o CSS do widget é carregado automaticamente dentro do Shadow DOM com base no nome do script (`widget.js` → `widget.css`). Se precisar forçar um caminho absoluto, defina a variável de ambiente `WIDGET_CSS_URL` durante o build do bundle standalone.

### Variáveis de ambiente do widget standalone

#### WIDGET_BASE_URL (desenvolvimento/local)

Durante o desenvolvimento, é comum servir o widget diretamente do projeto 3 (`olive-face-heart-app/public/widget`). Você pode apontar o SDK para esse servidor local definindo `WIDGET_BASE_URL` no build do SDK ou no ambiente em que o bundle React será servido.

Exemplo (servindo o widget em http://localhost:8080/widget/):

```bash
# .env.development
WIDGET_BASE_URL=http://localhost:8080/widget/
```

O SDK validará o `targetOrigin` com base nessa URL para o handshake via `MessageChannel`.
 `WIDGET_NAME`: nome do arquivo JS de saída (ex.: `vitals-widget.js`)
- `WIDGET_CSS_URL` (opcional): URL absoluta do CSS; se não definido, o CSS é resolvido dinamicamente com base na URL do script atual.
- `WIDGET_BASE_URL` (novo em 0.4.0): URL base do iFrame que hospeda o widget (padrão seguro: `https://vitals.oliv-e.health/widget/`). Útil para desenvolvimento/local.


## Build, testes e publicação

Scripts principais:

- `pnpm build`            → build da lib (ESM, CJS, d.ts) via `vite.config.lib.ts`
- `pnpm build:lib`        → build somente da lib
- `pnpm build:widget`     → build do bundle standalone (IIFE)
- `pnpm build:widget:production` → build do bundle standalone em modo prod (.env.production)
- `pnpm serve`            → serve página de teste `./test` (porta 3333)
- `pnpm serve:widget`     → serve `./dist` (porta 3334)
- `pnpm typecheck`        → checagem de tipos
- `pnpm lint` / `pnpm format`

Publicação no npm (escopo @oliv-e):

1) Crie/tenha acesso à organização `oliv-e` no npm (reserva o escopo `@oliv-e`).
2) Garanta login e 2FA conforme sua política.
3) O package já possui:
   - `"files": ["dist"]`
   - `"types": "./dist/index.d.ts"`
   - `"exports"` para ESM/CJS/Types
   - `"prepublishOnly": "pnpm run build && pnpm run build:widget"`
4) Publique:

```bash
npm publish --access public
# ou, se 2FA write:
# npm publish --access public --otp <codigo>
```


## Atributos (obrigatórios e opcionais)

React (componente) vs Custom Element (HTML)
- clientKey (React) / client-key (Custom Element) – obrigatório. Identificador público do parceiro. Não inclua segredos.
- user (React e Custom Element) – obrigatório. CPF do usuário.
- queryClient (React) – opcional. Instância do QueryClient para compartilhamento de cache; se ausente, o SDK cria um internamente.
- birthyear – opcional. Ano de nascimento (number no React; string numérica no HTML).
- height – opcional. Altura em cm (number no React; string no HTML).
- weight – opcional. Peso em kg (number no React; string no HTML).
- sex – opcional. 0 = female, 1 = male.
- bp_group – opcional. 'primary' | 'normal' | 'prehypertension' | 'hypertension'.
- bp_mode – opcional. 'normal' | 'binary' | 'ternary'.
- facing_mode (React) / facing-mode (Custom Element) – opcional. Padrão: 'user'.

Notas
- Parâmetros são enviados ao iFrame apenas quando definidos; valores ausentes não são incluídos na query string.

## Tipos exportados

```ts
// Tipos base exportados pela biblioteca
export type Sex = 0 | 1; // 0=female, 1=male
export type FacingMode = 'user' | 'environment';

// Mantemos valores mais amplos para compatibilidade com integrações existentes
export type BPMode = 'normal' | 'binary' | 'ternary';
export type BPGroup = 'primary' | 'normal' | 'prehypertension' | 'hypertension';

export interface OnErrorPayload {
  code: string | null;
  step: 'sdk_init' | 'startPreview' | 'startMeasuring' | 'onEvent' | 'modal' | null;
  reason: 'FACE_LOSS' | 'ABNORMAL_RESULT' | 'MAX_MEASURE_TIME' | 'connect_failed' | 'path_not_found' | 'reject' | null;
  message?: string | null;
}
export interface OnReadyPayload { ready: boolean; source?: string }

export interface VitalsConfig {
  client: string;
  name: string;
  theme: string;
  // CPF do usuário (obrigatório)
  user: string;
  height?: number;
  weight?: number;
  birthyear?: number;
  sex?: 0 | 1;
  bp_mode?: 'normal' | 'binary' | 'ternary';
  bp_group?: 'primary' | 'normal' | 'prehypertension' | 'hypertension';
  facing_mode?: 'user' | 'environment';
  scanOnly?: boolean;
}

export type WidgetEventName =
  | 'vh:handshake'
  | 'vh:result'
  | 'vh:dispose'
  | 'vh:onerror'
  | 'vh:onsuccess'
  | 'vh:onready'
  | 'vh:onevent';
export interface HandshakePayload { ok: boolean }
export type ResultPayload = unknown;

export interface WidgetEventMap {
  'vh:handshake': CustomEvent<HandshakePayload>;
  'vh:result': CustomEvent<ResultPayload>;
  'vh:dispose': CustomEvent<void>;
  'vh:onerror': CustomEvent<OnErrorPayload>;
  'vh:onsuccess': CustomEvent<unknown>;
  'vh:onready': CustomEvent<OnReadyPayload>;
  'vh:onevent': CustomEvent<unknown>;
}

export interface WidgetContainerProps extends VitalsConfig {}
export interface WidgetProps {}
```


## Eventos e comunicação

API imperativa e comandos (start/stop)
- Via ref do componente (recomendado). Observação: initScan só envia a mensagem após o iFrame emitir OnReady; antes disso, a chamada é ignorada.

```tsx
import { useRef } from 'react';
import Vitals, { type WidgetControls } from '@oliv-e/vitals';

export default function Page() {
  const ref = useRef<WidgetControls>(null);
  return (
    <>
      <Vitals ref={ref} clientKey="<CLIENT_KEY>" user="14267197016" />
      <button onClick={() => ref.current?.initScan()}>Iniciar</button>
      <button onClick={() => ref.current?.stopScan()}>Parar</button>
    </>
  );
}
```

Também é possível via eventos de janela (legado):
- vh:startScan → envia InitScan ao iFrame (use quando scanOnly=true para iniciar sob demanda)
- vh:stopScan → envia StopScan ao iFrame

Exemplo:
```ts
window.dispatchEvent(new Event('vh:startScan'));
```

O componente React `<Widget />` cria um `MessageChannel` com o iFrame. O iFrame envia mensagens que são propagadas para `window` como eventos customizados:

- `vh:handshake` – canal pronto
- `vh:onready` – iFrame pronto para iniciar a medição (equivalente ao evento OnReady do widget)
- `vh:result` – resultados de medição contínuos (payload depende do backend)
- `vh:onsuccess` – medição finalizada com sucesso (payload: resultado final)
- `vh:onerror` – erros operacionais/SDK (payload com code/step/reason/message)
- `vh:onevent` – espelha eventos gerais do SDK (ex.: camera_ready, connection_close)
- `vh:dispose` – canal encerrado

Exemplo de listener no app host:

```ts path=null start=null
useEffect(() => {
  function onResult(e: CustomEvent) {
    console.log('Resultado:', e.detail);
  }
  window.addEventListener('vh:result' as any, onResult as any);
  return () => window.removeEventListener('vh:result' as any, onResult as any);
}, []);
```


## Configuração de estilos (Tailwind/CSS)

- Os estilos do widget são gerados em build a partir de `src/widget/styles/style.css` (usa `@apply`).
- `tailwind.config.cjs` define `corePlugins.preflight = false` para evitar reset global.
- A lib exporta `dist/vitals-widget.css`. Se o host não importar CSS de node_modules automaticamente, importe manualmente.


## Dicas de troubleshooting

- `Scope not found` ao publicar: crie a org `oliv-e` no npm ou use seu escopo de usuário.
- CSS: o build gera `dist/vitals.css` para a lib; para o widget standalone, o CSS é injetado automaticamente.
- Tipos não aparecem no pacote: rode `pnpm build` antes do publish (gera `dist/index.d.ts`).
- SSR: os acessos a `window` ocorrem dentro de efeitos; evite renderizar o iFrame no server.
- Múltiplos QueryClients: se o app host já usa React Query e o SDK criar outro, haverá caches isolados e possivelmente chamadas duplicadas. Prefira passar o `QueryClient` do app via prop `queryClient` ou envolver o SDK em um único `QueryClientProvider` no topo.


## Licença

MIT. Veja [LICENSE](./LICENSE).