@oliv-e/vitals
v0.4.1
Published
Componentes React do Oliv-e Vitals
Downloads
66
Readme
@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 ()
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 compostMessage + MessageChannelpara receber resultados. Essa URL pode ser customizada viaWIDGET_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.mdUso como biblioteca React (npm)
Instalação (após publicação):
pnpm add @oliv-e/vitals @tanstack/react-queryNota: @tanstack/react-query é peerDependency do SDK.
Exemplo recomendado (default export):
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 headerx-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>eContent-Type: application/json - Body:
{ ...dadosDoEvento, metadata: { Cpf: "<CPF do usuário>" } }
- Header:
- 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
QueryClientinterno e envolve o widget emQueryClientProvider. - Para evitar múltiplos caches e requisições duplicadas, recomenda-se passar o
QueryClientda aplicação host.
Exemplos:
Usando QueryClient da aplicação:
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:
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):
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:
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:
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:
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
initScanestopScan - Widget: componente que renderiza somente o iFrame (sem Providers); útil dentro do Provider
Exemplo:
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 deWidgetControls)
Uso via SCRIPT TAG (widget standalone)
Build do widget (dev/prod):
pnpm build:widget
pnpm build:widget:productionServir e testar:
pnpm serve:widget # serve ./dist na porta 3334
pnpm serve # serve ./test na porta 3333Abra http://localhost:3333/test/index.html — o HTML de teste carrega http://localhost:3334/widget.js e instância o Custom Element:
<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-keydeve existir nesse arquivo para que o widget funcione.
Exemplo mínimo (client-key + user):
<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):
<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/):
# .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) viavite.config.lib.tspnpm build:lib→ build somente da libpnpm 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 tipospnpm lint/pnpm format
Publicação no npm (escopo @oliv-e):
- Crie/tenha acesso à organização
oliv-eno npm (reserva o escopo@oliv-e). - Garanta login e 2FA conforme sua política.
- O package já possui:
"files": ["dist"]"types": "./dist/index.d.ts""exports"para ESM/CJS/Types"prepublishOnly": "pnpm run build && pnpm run build:widget"
- Publique:
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
// 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.
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:
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 prontovh: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:
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.cjsdefinecorePlugins.preflight = falsepara 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 foundao publicar: crie a orgoliv-eno npm ou use seu escopo de usuário.- CSS: o build gera
dist/vitals.csspara a lib; para o widget standalone, o CSS é injetado automaticamente. - Tipos não aparecem no pacote: rode
pnpm buildantes do publish (geradist/index.d.ts). - SSR: os acessos a
windowocorrem 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
QueryClientdo app via propqueryClientou envolver o SDK em um únicoQueryClientProviderno topo.
Licença
MIT. Veja LICENSE.
