npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@bravophone/webphone

v0.7.1

Published

Webphone BRAVOPHONE embutível em qualquer página web. Renderiza uma janela flutuante arrastável com todas as funcionalidades do softphone.

Readme

@bravophone/webphone

Webphone BRAVOPHONE embutível em qualquer página web: o softphone aparece como uma janela flutuante arrastável, com as mesmas funcionalidades da extensão de navegador.

<script>
// Resolve a versão publicada e carrega o bundle dela.
// Duas etapas de propósito: a consulta tem 5 min de cache, o bundle é
// immutable. Assim as correções chegam em minutos, sem revalidar 30 kB a cada
// visita — e sem os 7 dias de cache que a URL sem versão carrega.
// cache:'no-store' na CONSULTA (~1 kB): sem ele, a resposta fica até 5 min
// no navegador e uma publicação recém-saída não aparece — foi o que exigiu
// Ctrl+Shift+R nos testes. O bundle continua vindo de cache immutable, então
// o custo é uma requisição pequena por carregamento, não 30 kB.
fetch('https://data.jsdelivr.com/v1/packages/npm/@bravophone/webphone/resolved',
      { cache: 'no-store' })
  .then((r) => r.json())
  .then(({ version }) => {
    const s = document.createElement('script')
    s.src = `https://cdn.jsdelivr.net/npm/@bravophone/webphone@${version}/dist/bravophone.umd.js`
    s.onload = () => Bravophone.init({ session: SESSAO_DO_LOGIN })
    s.onerror = () => console.error('Bravophone: falha ao carregar do CDN')
    document.head.appendChild(s)
  })
</script>

Cole inline, não como arquivo externo — um arquivo externo teria o mesmo problema de cache que este trecho existe para evitar. Detalhes em Manter o cliente sempre atualizado.

Para travar numa versão (integração de terceiros, ou build com SRI):

<script src="https://cdn.jsdelivr.net/npm/@bravophone/[email protected]/dist/bravophone.umd.js"></script>
<script>Bravophone.init({ token: TOKEN_DO_USUARIO })</script>
// ou via npm
import Bravophone from '@bravophone/webphone'

Bravophone.init({ token })
Bravophone.on('call:incoming', ({ number }) => console.log('ligação de', number))
await Bravophone.call('11987654321')

A decisão de arquitetura

O ponto de partida é uma restrição concreta: popup.js tem 932 KB de build Vue minificado e o código-fonte não está disponível. Recompilar não é uma opção, então o projeto foi desenhado para reaproveitar o bundle exatamente como está.

O levantamento do uso de chrome.* no bundle mostrou que isso é viável — a superfície é pequena e concentrada:

| API | Usos | Tratamento | |---|---|---| | chrome.storage.sync | 58 | shim → localStorage | | chrome.storage.local | 10 | shim → localStorage | | chrome.storage.onChanged | 4 | shim → emissor próprio | | chrome.runtime.onMessage | 3 | shim → barramento local | | chrome.tabs.create | 3 | shim → window.open | | chrome.windows.* | 6 | shim → delega ao widget via bridge |

São três superfícies reais, todas sem estado remoto. Um shim de ~230 linhas cobre todas — é o host/shim/chrome-shim.js.

Dois artefatos, não um

┌─ SITE DO CLIENTE (qualquer origem) ────────────────────────┐
│                                                             │
│   <script src="cdn.../@bravophone/webphone">                │
│            │                                                │
│            ▼                                                │
│   ┌─ SDK (11 KB) ──────────────────────┐                    │
│   │  Shadow DOM · janela arrastável    │                    │
│   │  API pública · ponte postMessage   │                    │
│   │                                    │                    │
│   │   ┌─ <iframe srcdoc> ───────────┐  │                    │
│   │   │  origem: a do próprio site  │  │                    │
│   │   │  arquivos: CDN (jsDelivr)   │  │                    │
│   │   │                             │  │                    │
│   │   │  chrome-shim.js             │  │                    │
│   │   │  libwebphone.js   (604 KB)  │  │                    │
│   │   │  popup.js         (932 KB)  │  │  ← bundle intacto  │
│   │   │  guest-bridge.js            │  │                    │
│   │   └─────────────────────────────┘  │                    │
│   └────────────────────────────────────┘                    │
└─────────────────────────────────────────────────────────────┘

O SDK no npm/CDN é leve (11 KB / 4,6 KB gzip). Todo o peso do webphone fica no host e carrega sob demanda, quando o usuário abre a janela.

Por que iframe, e não montar o Vue direto na página

Testei mentalmente as duas rotas; o iframe ganha em quatro frentes de uma vez:

  1. CSS. O bundle traz Tailwind + dark-theme.css globais. Injetado na página do cliente, ele quebraria o site do cliente — e o CSS do cliente quebraria o webphone.
  2. CORS, e este é o argumento decisivo. Dentro do iframe, todo request para pabx.teambravotech.com e devices.wavoip.com sai com Origin: https://webphone.bravophone.comuma origem só, fixa. Sem iframe, cada cliente novo exigiria liberar mais uma origem no CORS de três backends. Com iframe, a lista de origens do backend nunca cresce.
  3. Microfone. allow="microphone" no iframe é um contrato explícito e auditável.
  4. Atualização. Corrigiu algo no webphone? Republique o host. Todos os clientes recebem sem trocar a versão do pacote npm.

O que muda em relação à extensão

Login: o ponto que exige decisão de produto

Este é o único item que não tem solução puramente técnica, e vale ler antes de começar.

Desde o Chrome 115, o storage partitioning é padrão: o localStorage de um iframe cross-origin é particionado pelo site que o contém. Na prática, um usuário logado no webphone em clienteA.com não estará logado em clienteB.com — mesmo sendo o mesmo iframe, o mesmo usuário e a mesma origem. A extensão nunca teve esse problema porque tinha um storage único.

Três caminhos, em ordem de recomendação:

  1. Token do integrador (recomendado). O backend do cliente emite um token de sessão e passa em Bravophone.init({ token }). O SDK entrega ao iframe pela ponte e o guest-bridge grava onde o bundle já procura (vxToken). Sem tela de login, sem depender de cookie de terceiros, e é o modelo que Intercom/Twilio usam. Combina bem com o fato de que o vxToken é eterno — só logout explícito o encerra.
  2. Login em popup window. window.open para a origem do webphone (contexto first-party, sem partição), token volta por postMessage. Bom se não houver backend do lado do cliente.
  3. Storage Access API. Exige gesto do usuário e o suporte varia entre navegadores. Serve como fallback, não como plano principal.

O SDK já implementa o caminho 1 de ponta a ponta.

Funcionalidades que não portam

Os ~25 content-script-*.js (Pipedrive, HubSpot, Kommo, Salesforce…) injetam click-to-call em CRMs de terceiros. Isso é território exclusivo de extensão — uma biblioteca só roda onde foi incluída.

A substituição é a inversão do controle: em vez de o Bravophone entrar no CRM, o CRM chama o Bravophone.

document.querySelectorAll('[data-phone]').forEach((el) => {
  el.onclick = () => Bravophone.call(el.dataset.phone, { source: 'crm', id: el.dataset.id })
})

Também ficam de fora contextMenus (menu de contexto do navegador), devtools.js e a leitura de clipboard sem gesto do usuário.

O que se mantém idêntico

Registro SIP, áudio WebRTC, supressão de ruído, seleção de rota, histórico, contatos, transferência, DTMF, e a normalização de número — inclusive a regra de nunca inserir o 9º dígito: dialpad.call() continua sendo o funil único de ligações, então toda essa lógica é exatamente a mesma da extensão.


Estrutura

Bravophone-SDK/
├── src/                    ← vira o pacote npm (11 KB)
│   ├── index.js              API pública + registro de eventos
│   ├── widget.js             Shadow DOM, iframe, launcher
│   ├── draggable.js          arraste/resize com Pointer Events + persistência
│   ├── bridge.js             RPC postMessage (lado host)
│   └── styles.js             CSS isolado do widget
│
├── host/                   ← vira webphone.bravophone.com (RAIZ do domínio)
│   ├── index.html            gerado pelo sync (ordem de scripts importa)
│   ├── shim/chrome-shim.js   emula chrome.* para o bundle
│   ├── shim/guest-bridge.js  RPC (lado iframe) + eventos + arraste interno
│   ├── allowed-origins.json  origens autorizadas a embutir
│   ├── popup.js  js/  css/   ┐ copiados da extensão pelo sync,
│   ├── fonts/  images/       ┘ na RAIZ — não versionados (ver abaixo)
│   ├── mock.html             ┐ só desenvolvimento:
│   └── mock-webphone.js      ┘ webphone falso, sem SIP nem backend
│
├── scripts/
│   ├── sync-from-extension.mjs   copia os assets da extensão
│   ├── dev-server.mjs            duas origens locais (5173 / 5174)
│   └── smoke-shim.mjs            testes do chrome-shim
├── types/index.d.ts
└── examples/
    ├── test.html             painel de teste completo (usa o build UMD)
    └── basic.html            exemplo mínimo de integração

A extensão é a fonte da verdade. Nada copiado é editado à mão. Quando a extensão for atualizada:

npm run sync          # host servido de um domínio próprio (public path "/")
npm run prepare:host  # host servido do CDN — é o que vai no pacote publicado

O prepare:host reescreve o public_path do bundle para cdn.jsdelivr.net/npm/@bravophone/webphone@<versão>/host/. Rode-o depois de cada bump de versão e antes de publicar: se a URL apontar para outra versão, o pacote busca assets que podem não existir. O npm test recusa esse descompasso.

O script recusa rodar se um asset obrigatório sumir, em vez de gerar um host quebrado silenciosamente.

Por que os assets ficam na raiz de host/, e não num vendor/

O bundle foi buildado com __webpack_public_path__ = "/". Duas fontes são resolvidas por esse caminho absoluto:

n.p + "fonts/Audiowide-Regular.ttf"   // Audiowide — a fonte da marca
n.p + "fonts/Seguiemj.ttf"            // SegoeUIEmoji — os emojis

Sob um subdiretório, esses dois pedidos dão 404 e o navegador cai no fallback silenciosamente — sem erro visível, só a tipografia errada, e justamente depois do login, que é onde a Audiowide aparece. Replicar o layout de URL da extensão faz tudo resolver sem tocar no bundle: npm run sync termina verificando que as duas fontes aterrissaram em /fonts/, e falha alto se não.

Consequência de deploy: o host precisa ficar na raiz de um domínio ou subdomínio. Para servir sob um subpath, use npm run sync -- --public-path=/embed/ — troca só essa constante no bundle, de forma determinística e refeita a cada sync (e aborta se não encontrar exatamente uma ocorrência, em vez de adivinhar).


Desenvolvimento

Pré-requisito: a extensão ao lado

Este repositório não versiona o webphone — só o SDK e a camada que faz o bundle da extensão rodar fora dela. O popup.js (932 KB), o libwebphone.js, as fontes e os _locales são copiados da extensão pelo npm run sync e ficam fora do git.

Clone os dois como irmãos:

algum-diretorio/
├── Bravophone/       ← a extensão (fonte da verdade do webphone)
└── bravophone-sdk/   ← este repositório
git clone https://github.com/teambravotech/bravophone-sdk.git
cd bravophone-sdk
npm install
npm run sync      # copia os assets da extensão irmã
npm run build     # gera dist/ (ESM + UMD + sourcemaps)
npm start         # sobe as duas origens de teste

Se a extensão estiver em outro lugar, passe o caminho: npm run sync -- /caminho/para/Bravophone.

Sem o sync, o host não tem o que servir — npm start sobe, mas o webphone real não carrega (o mock em ?host=mock continua funcionando).

Scripts

| Script | O que faz | |---|---| | npm run sync | Copia os assets da extensão, gera host/index.html e shim/messages.js, e roda a auditoria de tema | | npm run build | Gera dist/ — o que vai para o npm | | npm start | Sobe as duas origens locais (5173 site, 5174 host) | | npm test | 100 asserções: shim, geometria da janela e aba de abertura | | npm run audit:theme | Procura texto invisível no tema escuro | | npm run purge | Limpa o cache do CDN nas URLs sem versão fixa (roda sozinho após o publish) |

Abra http://localhost:5173/.

O npm start sobe duas portas de propósito — origens diferentes fazem o teste exercitar o postMessage cross-origin de verdade, incluindo a validação de origem:

| Porta | Papel | Serve | |---|---|---| | 5173 | site do cliente | examples/test.html, carrega dist/bravophone.umd.js por <script>, como no CDN | | 5174 | host do webphone | host/mock.html, com os headers frame-ancestors e Permissions-Policy de produção |

Testar sem SIP nem backend

O host/mock.html carrega o chrome-shim.js e o guest-bridge.js reais e troca só o bundle por host/mock-webphone.js, que expõe os mesmos dois handles que o guest-bridge procura (window.dialpad e window.libwebphone). Ou seja: o caminho testado é o de produção, sem depender de registro SIP.

Dá para verificar ponta a ponta o arraste e o resize, a persistência da posição, os comandos (call/hangup/mute/transfer…), os eventos de volta, uma chamada entrante abrindo a janela sozinha, e o init({ token }) chegando ao storage via shim — o painel do mock mostra o vxToken gravado.

Para testar contra o webphone real, rode npm run sync e aponte o hostUrl do examples/test.html para http://localhost:5174/index.html em vez de mock.html.

npm test          # 18 asserções sobre o chrome-shim, sem browser

Deploy

1. Host — webphone.bravophone.com (raiz)

Estático (S3+CloudFront, Vercel, nginx). Três headers importam:

Content-Security-Policy: frame-ancestors 'self' https://clienteA.com https://clienteB.com;
Permissions-Policy: microphone=(self)
Cross-Origin-Opener-Policy: same-origin-allow-popups

frame-ancestors é o que impede qualquer site de embutir o webphone — deve ser gerado a partir de allowed-origins.json. É a mesma disciplina de autorização por origem que a extensão já adota; não troque por *.

Cache: popup.js, js/*, fonts/* com max-age=31536000 (invalide o CDN a cada sync, ou versione por query string); index.html sempre com no-cache.

2. Backends — CORS

Liberar uma única origem em api.bravophone.com, pabx.teambravotech.com e devices.wavoip.com:

Access-Control-Allow-Origin: https://webphone.bravophone.com
Access-Control-Allow-Credentials: true

Como o iframe tem origem fixa, essa lista não cresce com o número de clientes.

3. npm

npm publish --access public

Disponível em cdn.jsdelivr.net/npm/@bravophone/webphone e unpkg.com logo após. Recomende aos integradores a versão travada — @bravophone/[email protected] — para que um major não quebre a página deles.


Manter o cliente sempre atualizado

A intuição diz para usar a URL sem versão. É a pior escolha para isso, e os headers do CDN mostram por quê:

sem versão / @0.2   →  max-age=604800   (7 dias no navegador do usuário)
@0.2.1 exata        →  immutable        (eterno, mas fixo)

A URL sem versão é entregue com sete dias de cache na máquina de quem acessa. Publicar uma correção não alcança essa pessoa: npm run purge limpa as bordas do CDN, não o cache que já está no navegador dela.

O caminho que resolve é examples/loader-latest.js, que separa as duas coisas:

  1. pergunta ao CDN qual é a versão atual — resposta com 5 min de cache;
  2. carrega o bundle daquela versão exata — URL imutável, cache eterno.

Uma publicação chega em até cinco minutos, e o arquivo pesado vem de um cache que nunca precisa ser revalidado. O custo é uma requisição de ~1 kB antes do bundle, quase sempre servida do cache.

const { version } = await (await fetch(
  'https://data.jsdelivr.com/v1/packages/npm/@bravophone/webphone/resolved'
)).json()

const s = document.createElement('script')
s.src = `https://cdn.jsdelivr.net/npm/@bravophone/webphone@${version}/dist/bravophone.umd.js`
document.head.appendChild(s)

Cole isso inline na página, não como arquivo externo — um arquivo externo teria o mesmo problema de cache que estamos evitando.

Se uma publicação não aparecer

Três caches diferentes, do mais provável ao menos:

| O que está velho | Como saber | Solução | |---|---|---| | A consulta de versão | Bravophone.version mostra a anterior | Já resolvido: o snippet usa cache: 'no-store' | | A página do integrador | o próprio snippet mudou e não teve efeito | Não sirva o HTML com max-age longo | | O bundle | — | Não acontece: a URL é versionada e immutable |

Durante o desenvolvimento, Ctrl+Shift+R limpa os três de uma vez — foi o que funcionou nos primeiros testes. Em produção não há como pedir isso ao usuário, e é por isso que o no-store está na consulta: sem ele, quem carregou a página nos últimos cinco minutos continua na versão anterior.

Se ainda assim algo ficar para trás, npm run purge limpa as bordas do CDN — mas lembre que ele não alcança o navegador de ninguém.

Os assets do webphone acompanham automaticamente: o public_path é gravado com a versão do pacote, então carregar o SDK 0.2.1 carrega o host 0.2.1.

A sessão que o webphone espera

init() recebe a resposta do /api/voxfree/login inteira:

Bravophone.init({
  session: {
    vxToken:    '…',   // obrigatório
    expiresIn:  3600,  // segundos
    sip:        '…',   // sem isto o webphone não registra
    ramal:      '…',   // idem
    tenant:     '…',
    clienteId:  '…',
    ramaisUrl:  '…',

    // A segunda metade: sem ela o app fica na tela de login, mesmo com o
    // vxToken válido. O checkToken do webphone exige as duas.
    extension: { username: '…', password: '…', server: '…' },
  },
})

Onde a credencial SIP fica. O extension viaja apenas pela ponte (postMessage) e é aplicado no store em memória do webphone. Ele não entra no HTML do iframe nem no localStorage — a senha não fica legível no DOM da sua página. As outras sete chaves são pré-gravadas no storage, porque é de lá que o bundle as lê.

O SDK grava essas chaves onde o bundle as procura, antes dele avaliar — a sessão já sobe autenticada, sem piscar a tela de login.

token: '…' continua aceito como atalho para { vxToken }, mas sozinho não basta: o webphone carrega, não registra, e o RouteSelector avisa "faça login pelo webphone" — justamente o que a auto-autenticação existe para evitar.

Documentação

| Documento | Para quem | |---|---| | docs/api.html | Referência completa da API — abra no navegador | | docs/PARA-IA.md | Contexto para um agente de IA implementar a integração | | examples/integracao.html | Exemplo pronto para colar numa página | | examples/loader.js | Carregar o SDK por JavaScript (SPA, Tag Manager) |

API

Bravophone.init(options)

| Opção | Tipo | Padrão | Descrição | |---|---|---|---| | token | string | — | Token de sessão emitido pelo seu backend | | mode | 'srcdoc' \| 'hosted' | 'srcdoc' | Como o webphone é carregado — ver abaixo | | hostUrl | string | https://webphone.bravophone.com/ | Origem do webphone (só no modo hosted) | | position | string | 'bottom-right' | Canto inicial | | open | boolean | false | Abrir já visível | | launcher | boolean | true | Exibir a aba lateral de abertura | | launcherSide | 'right' \| 'left' | 'right' | Lado em que a aba fica colada | | frame | 'none' \| 'bar' | 'none' | Moldura da janela — ver abaixo | | dockTop | 'max' \| 'top-half' | 'max' | O que arrastar até a borda superior faz | | title | string | 'BRAVOPHONE' | Texto da barra (só com frame: 'bar') |

Moldura: preservando 100% da UI

Por padrão (frame: 'none') não há barra de título — a UI do popup.js ocupa a janela inteira, exatamente como na extensão. Nenhum pixel é tomado.

O arraste continua funcionando porque a detecção do gesto acontece dentro do iframe, no guest-bridge.js: o host é cross-origin e não pode tocar naquele DOM, então o gesto viaja como delta pela ponte. Qualquer área que não seja botão, campo ou link arrasta a janela; o resto continua clicável, e uma seleção de texto em andamento nunca é sequestrada. As coordenadas usam screenX/screenY — absolutas na tela, imunes ao fato de o próprio iframe estar se movendo durante o arraste.

Um botão de fechar aparece sobreposto no canto ao passar o mouse, sem empurrar o conteúdo. Recolher para o launcher faz o papel de minimizar.

Use frame: 'bar' se preferir a barra com título, indicador de estado e controles.

Métodos

Janelashow() · hide() · toggle() · minimize(force?) · move(x, y) · resize(w, h) · dock(zone) · destroy() · isOpen · geometry

Dois modos de carregar o webphone

Bravophone.init({ token })                     // hospedado (padrão)
Bravophone.init({ token, mode: 'srcdoc' })     // na origem do próprio site

| | srcdoc (padrão) | hosted | |---|---|---| | Onde o iframe roda | webphone.bravophone.com | origem do próprio site | | De onde vêm os arquivos | do host | do CDN, travados nesta versão | | CORS dos backends | uma origem fixa, não cresce | uma entrada por integrador | | Iframe de terceiro | sim — sujeito a bloqueador e política | não | | Storage particionado | sim (login por site do cliente) | não | | Você precisa manter | o domínio do host | nada |

Antes de oferecer o srcdoc a um cliente, a origem dele precisa estar na allowlist de CORS de api.bravophone.com, pabx.teambravotech.com e devices.wavoip.com. Sem isso o webphone carrega, aparece na tela e não registra — o navegador descarta as respostas. Isso é trabalho no nosso backend: o integrador não tem como liberar CORS de um servidor que não é dele.

Há requests com withCredentials, então Access-Control-Allow-Origin: * não serve: a resposta precisa ecoar a origem exata mais Access-Control-Allow-Credentials: true. O caminho sustentável é a allowlist sair de banco, para entrar um cliente ser um registro e não um deploy em três serviços.

Do lado do integrador, o único requisito é o CSP admitir cdn.jsdelivr.net em script-src, style-src e font-src — a maioria dos sites não tem CSP restritivo e não precisa fazer nada.

O que foi verificado em navegador (examples/srcdoc-validation.html): getUserMedia funciona dentro do srcdoc sem allow=, a permissão é herdada do topo, enumerateDevices traz os rótulos, localStorage funciona, document.baseURI resolve para a página pai e @font-face com URL absoluta do CDN carrega.

A aba de abertura

Com a janela fechada, o webphone fica acessível por uma aba colada na lateral da viewport — não um botão circular solto no canto. Ela é arrastável na vertical e guarda a posição entre sessões.

No repouso mostra só o ícone. No hover (ou com foco de teclado) ela expande e revela a alça de pontinhos, sinalizando que dá para arrastar.

Um detalhe que decide se o componente é agradável ou irritante: arrastar não abre o webphone. O gesto vira arraste depois de 4px percorridos; abaixo disso continua sendo clique. Sem esse limiar, uma tremida de mouse no clique abriria a janela sem querer — ou pior, todo arraste terminaria abrindo.

A aba também responde a teclado (Enter / Espaço) e, numa chamada entrante, pulsa em vermelho com o contador — visível mesmo com a janela fechada.

Bravophone.init({ launcherSide: 'left' })   // cola do outro lado
Bravophone.setLauncherSide('right')         // troca em runtime
Bravophone.init({ launcher: false })        // sem aba: você controla com show()

Redimensionar e encaixar

A janela redimensiona por qualquer borda ou canto — as alças laterais são o que permite alargar a janela para o histórico de chamadas respirar. O teto é a viewport, não um valor fixo.

Dois comportamentos de encaixe, ambos com o mesmo vocabulário do Canva:

Arrastando, encostar numa região da viewport mostra uma prévia azul do encaixe antes de soltar:

| Onde o cursor chega | Encaixe | |---|---| | borda esquerda / direita | altura cheia, largura mantida | | borda inferior | metade inferior, largura cheia | | borda superior | maximizado (configurável) | | os quatro cantos | meia tela esquerda/direita | | qualquer outro lugar | segue flutuando |

 left-half │    max     │ right-half
 ──────────┼────────────┼──────────
   left    │  (flutua)  │   right
 ──────────┼────────────┼──────────
 left-half │bottom-half │ right-half

A borda superior é a única disputada: max é o gesto universal (Windows, macOS), mas quem trabalha com metades verticais costuma preferir a metade de cima ali. Daí a opção dockTop:

Bravophone.init({ dockTop: 'top-half' })   // topo encaixa na metade superior

Com ela, o máximo continua acessível por dock('max').

Arrastar uma janela encaixada de volta para o meio a solta e devolve o tamanho que ela tinha antes — e a janela nasce sob o cursor, proporcional a onde você a pegou, em vez de saltar.

Redimensionando, chegar a ~32px de uma borda da viewport completa até ela sozinha — o "completamento sugestivo".

Programaticamente:

Bravophone.dock('right')        // altura cheia à direita, largura mantida
Bravophone.dock('right-half')   // metade direita  (W/2 × altura cheia)
Bravophone.dock('bottom-half')  // metade inferior (largura cheia × H/2)
Bravophone.dock('top-half')     // metade superior
Bravophone.dock('bottom')       // metade inferior, mantendo a largura atual
Bravophone.dock('max')          // maximiza
Bravophone.dock('float')        // solta e restaura o tamanho anterior

Bravophone.on('resize', ({ width, height, dock }) => { /* … */ })

O encaixe persiste entre sessões e é recalculado para a viewport atual ao recarregar — uma janela docada ontem numa tela larga não volta com a geometria de ontem.

Telefonia — todos retornam Promise: call(number, meta?) · hangup() · answer() · mute(on?) · hold(on?) · sendDTMF(tone) · transfer(to) · getStatus() · setAuth(token) · logout()

Eventos

const off = Bravophone.on('call:incoming', (call) => { /* … */ })
off()  // remove o listener

ready · state · call:dialing · call:incoming · call:answered · call:ended · resize · reveal · open · close · error. Use '*' para receber todos como { event, payload }.

Uma chamada que não completa chega como call:ended: o estado do bundle não distingue desligar de falhar.


Requisitos

  • HTTPS obrigatório no site do cliente — getUserMedia só existe em secure context. localhost funciona no desenvolvimento; o SDK avisa no console se detectar contexto inseguro.
  • Navegadores com WebRTC e Shadow DOM: Chrome/Edge 88+, Firefox 90+, Safari 14+.
  • O site do cliente não pode ter um CSP frame-src que bloqueie webphone.bravophone.com — vale documentar isso no onboarding.

Licença

Software proprietário da BravoTech. Todos os direitos reservados. O pacote npm é publicado para consumo pelos integradores; o código deste repositório não é open source.