@bravophone/webphone 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +479 -0
- package/dist/bravophone.js +917 -0
- package/dist/bravophone.umd.cjs +340 -0
- package/package.json +65 -0
- package/types/index.d.ts +153 -0
package/README.md
ADDED
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
# @bravophone/webphone
|
|
2
|
+
|
|
3
|
+
Webphone BRAVOPHONE embutível em qualquer página web. Uma linha de `<script>` e o
|
|
4
|
+
softphone aparece como uma janela flutuante arrastável, com as mesmas
|
|
5
|
+
funcionalidades da extensão de navegador.
|
|
6
|
+
|
|
7
|
+
```html
|
|
8
|
+
<script src="https://cdn.jsdelivr.net/npm/@bravophone/webphone"></script>
|
|
9
|
+
<script>
|
|
10
|
+
Bravophone.init({ token: 'TOKEN_DO_USUARIO' })
|
|
11
|
+
</script>
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
// ou via npm
|
|
16
|
+
import Bravophone from '@bravophone/webphone'
|
|
17
|
+
|
|
18
|
+
Bravophone.init({ token, position: 'bottom-right' })
|
|
19
|
+
Bravophone.on('call:incoming', ({ number }) => console.log('ligação de', number))
|
|
20
|
+
await Bravophone.call('11987654321')
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## A decisão de arquitetura
|
|
26
|
+
|
|
27
|
+
O ponto de partida é uma restrição concreta: **`popup.js` tem 932 KB de build Vue
|
|
28
|
+
minificado e o código-fonte não está disponível.** Recompilar não é uma opção, então
|
|
29
|
+
o projeto foi desenhado para **reaproveitar o bundle exatamente como está**.
|
|
30
|
+
|
|
31
|
+
O levantamento do uso de `chrome.*` no bundle mostrou que isso é viável — a
|
|
32
|
+
superfície é pequena e concentrada:
|
|
33
|
+
|
|
34
|
+
| API | Usos | Tratamento |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| `chrome.storage.sync` | 58 | shim → `localStorage` |
|
|
37
|
+
| `chrome.storage.local` | 10 | shim → `localStorage` |
|
|
38
|
+
| `chrome.storage.onChanged` | 4 | shim → emissor próprio |
|
|
39
|
+
| `chrome.runtime.onMessage` | 3 | shim → barramento local |
|
|
40
|
+
| `chrome.tabs.create` | 3 | shim → `window.open` |
|
|
41
|
+
| `chrome.windows.*` | 6 | shim → delega ao widget via bridge |
|
|
42
|
+
|
|
43
|
+
São **três superfícies reais**, todas sem estado remoto. Um shim de ~230 linhas cobre
|
|
44
|
+
todas — é o [`host/shim/chrome-shim.js`](host/shim/chrome-shim.js).
|
|
45
|
+
|
|
46
|
+
### Dois artefatos, não um
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
┌─ SITE DO CLIENTE (qualquer origem) ────────────────────────┐
|
|
50
|
+
│ │
|
|
51
|
+
│ <script src="cdn.../@bravophone/webphone"> │
|
|
52
|
+
│ │ │
|
|
53
|
+
│ ▼ │
|
|
54
|
+
│ ┌─ SDK (11 KB) ──────────────────────┐ │
|
|
55
|
+
│ │ Shadow DOM · janela arrastável │ │
|
|
56
|
+
│ │ API pública · ponte postMessage │ │
|
|
57
|
+
│ │ │ │
|
|
58
|
+
│ │ ┌─ <iframe> ──────────────────┐ │ │
|
|
59
|
+
│ │ │ origem FIXA: │ │ │
|
|
60
|
+
│ │ │ webphone.bravophone.com │ │ │
|
|
61
|
+
│ │ │ │ │ │
|
|
62
|
+
│ │ │ chrome-shim.js │ │ │
|
|
63
|
+
│ │ │ libwebphone.js (604 KB) │ │ │
|
|
64
|
+
│ │ │ popup.js (932 KB) │ │ ← bundle intacto │
|
|
65
|
+
│ │ │ guest-bridge.js │ │ │
|
|
66
|
+
│ │ └─────────────────────────────┘ │ │
|
|
67
|
+
│ └────────────────────────────────────┘ │
|
|
68
|
+
└─────────────────────────────────────────────────────────────┘
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**O SDK no npm/CDN é leve (11 KB / 4,6 KB gzip).** Todo o peso do webphone fica no
|
|
72
|
+
host e carrega sob demanda, quando o usuário abre a janela.
|
|
73
|
+
|
|
74
|
+
### Por que iframe, e não montar o Vue direto na página
|
|
75
|
+
|
|
76
|
+
Testei mentalmente as duas rotas; o iframe ganha em quatro frentes de uma vez:
|
|
77
|
+
|
|
78
|
+
1. **CSS.** O bundle traz Tailwind + `dark-theme.css` globais. Injetado na página do
|
|
79
|
+
cliente, ele quebraria o site do cliente — e o CSS do cliente quebraria o webphone.
|
|
80
|
+
2. **CORS, e este é o argumento decisivo.** Dentro do iframe, todo request para
|
|
81
|
+
`api.bravophone.com`, `reports.teambravotech.com` e `devices.wavoip.com` sai com
|
|
82
|
+
`Origin: https://webphone.bravophone.com` — **uma origem só, fixa**. Sem iframe,
|
|
83
|
+
cada cliente novo exigiria liberar mais uma origem no CORS de três backends. Com
|
|
84
|
+
iframe, a lista de origens do backend nunca cresce.
|
|
85
|
+
3. **Microfone.** `allow="microphone"` no iframe é um contrato explícito e auditável.
|
|
86
|
+
4. **Atualização.** Corrigiu algo no webphone? Republique o host. Todos os clientes
|
|
87
|
+
recebem sem trocar a versão do pacote npm.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## O que muda em relação à extensão
|
|
92
|
+
|
|
93
|
+
### Login: o ponto que exige decisão de produto
|
|
94
|
+
|
|
95
|
+
Este é o único item que **não** tem solução puramente técnica, e vale ler antes de
|
|
96
|
+
começar.
|
|
97
|
+
|
|
98
|
+
Desde o Chrome 115, o *storage partitioning* é padrão: o `localStorage` de um iframe
|
|
99
|
+
cross-origin é **particionado pelo site que o contém**. Na prática, um usuário logado
|
|
100
|
+
no webphone em `clienteA.com` **não** estará logado em `clienteB.com` — mesmo sendo o
|
|
101
|
+
mesmo iframe, o mesmo usuário e a mesma origem. A extensão nunca teve esse problema
|
|
102
|
+
porque tinha um storage único.
|
|
103
|
+
|
|
104
|
+
Três caminhos, em ordem de recomendação:
|
|
105
|
+
|
|
106
|
+
1. **Token do integrador (recomendado).** O backend do cliente emite um token de sessão
|
|
107
|
+
e passa em `Bravophone.init({ token })`. O SDK entrega ao iframe pela ponte e o
|
|
108
|
+
`guest-bridge` grava onde o bundle já procura (`vxToken`). Sem tela de login, sem
|
|
109
|
+
depender de cookie de terceiros, e é o modelo que Intercom/Twilio usam. Combina bem
|
|
110
|
+
com o fato de que [o `vxToken` é eterno](#) — só logout explícito o encerra.
|
|
111
|
+
2. **Login em popup window.** `window.open` para a origem do webphone (contexto
|
|
112
|
+
*first-party*, sem partição), token volta por `postMessage`. Bom se não houver
|
|
113
|
+
backend do lado do cliente.
|
|
114
|
+
3. **Storage Access API.** Exige gesto do usuário e o suporte varia entre navegadores.
|
|
115
|
+
Serve como *fallback*, não como plano principal.
|
|
116
|
+
|
|
117
|
+
O SDK já implementa o caminho 1 de ponta a ponta.
|
|
118
|
+
|
|
119
|
+
### Funcionalidades que não portam
|
|
120
|
+
|
|
121
|
+
Os ~25 `content-script-*.js` (Pipedrive, HubSpot, Kommo, Salesforce…) injetam
|
|
122
|
+
click-to-call em CRMs de terceiros. **Isso é território exclusivo de extensão** — uma
|
|
123
|
+
biblioteca só roda onde foi incluída.
|
|
124
|
+
|
|
125
|
+
A substituição é a inversão do controle: em vez de o Bravophone entrar no CRM, o CRM
|
|
126
|
+
chama o Bravophone.
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
document.querySelectorAll('[data-phone]').forEach((el) => {
|
|
130
|
+
el.onclick = () => Bravophone.call(el.dataset.phone, { source: 'crm', id: el.dataset.id })
|
|
131
|
+
})
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Também ficam de fora `contextMenus` (menu de contexto do navegador), `devtools.js` e a
|
|
135
|
+
leitura de clipboard sem gesto do usuário.
|
|
136
|
+
|
|
137
|
+
### O que se mantém idêntico
|
|
138
|
+
|
|
139
|
+
Registro SIP, áudio WebRTC, supressão de ruído, seleção de rota, histórico, contatos,
|
|
140
|
+
transferência, DTMF, e a **normalização de número** — inclusive a regra de
|
|
141
|
+
[nunca inserir o 9º dígito](#): `dialpad.call()` continua sendo o funil único de
|
|
142
|
+
ligações, então toda essa lógica é exatamente a mesma da extensão.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Estrutura
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
Bravophone-SDK/
|
|
150
|
+
├── src/ ← vira o pacote npm (11 KB)
|
|
151
|
+
│ ├── index.js API pública + registro de eventos
|
|
152
|
+
│ ├── widget.js Shadow DOM, iframe, launcher
|
|
153
|
+
│ ├── draggable.js arraste/resize com Pointer Events + persistência
|
|
154
|
+
│ ├── bridge.js RPC postMessage (lado host)
|
|
155
|
+
│ └── styles.js CSS isolado do widget
|
|
156
|
+
│
|
|
157
|
+
├── host/ ← vira webphone.bravophone.com (RAIZ do domínio)
|
|
158
|
+
│ ├── index.html gerado pelo sync (ordem de scripts importa)
|
|
159
|
+
│ ├── shim/chrome-shim.js emula chrome.* para o bundle
|
|
160
|
+
│ ├── shim/guest-bridge.js RPC (lado iframe) + eventos + arraste interno
|
|
161
|
+
│ ├── allowed-origins.json origens autorizadas a embutir
|
|
162
|
+
│ ├── popup.js js/ css/ ┐ copiados da extensão pelo sync,
|
|
163
|
+
│ ├── fonts/ images/ ┘ na RAIZ — não versionados (ver abaixo)
|
|
164
|
+
│ ├── mock.html ┐ só desenvolvimento:
|
|
165
|
+
│ └── mock-webphone.js ┘ webphone falso, sem SIP nem backend
|
|
166
|
+
│
|
|
167
|
+
├── scripts/
|
|
168
|
+
│ ├── sync-from-extension.mjs copia os assets da extensão
|
|
169
|
+
│ ├── dev-server.mjs duas origens locais (5173 / 5174)
|
|
170
|
+
│ └── smoke-shim.mjs testes do chrome-shim
|
|
171
|
+
├── types/index.d.ts
|
|
172
|
+
└── examples/
|
|
173
|
+
├── test.html painel de teste completo (usa o build UMD)
|
|
174
|
+
└── basic.html exemplo mínimo de integração
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**A extensão é a fonte da verdade.** Nada copiado é editado à mão. Quando a extensão
|
|
178
|
+
for atualizada:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
npm run sync # copia popup.js, libwebphone.js, css, fonts… (ignora .bak-*)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
O script recusa rodar se um asset obrigatório sumir, em vez de gerar um host quebrado
|
|
185
|
+
silenciosamente.
|
|
186
|
+
|
|
187
|
+
### Por que os assets ficam na raiz de `host/`, e não num `vendor/`
|
|
188
|
+
|
|
189
|
+
O bundle foi buildado com `__webpack_public_path__ = "/"`. Duas fontes são resolvidas
|
|
190
|
+
por esse caminho absoluto:
|
|
191
|
+
|
|
192
|
+
```js
|
|
193
|
+
n.p + "fonts/Audiowide-Regular.ttf" // Audiowide — a fonte da marca
|
|
194
|
+
n.p + "fonts/Seguiemj.ttf" // SegoeUIEmoji — os emojis
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Sob um subdiretório, esses dois pedidos dão 404 e o navegador cai no fallback
|
|
198
|
+
silenciosamente — sem erro visível, só a tipografia errada, e justamente **depois do
|
|
199
|
+
login**, que é onde a Audiowide aparece. Replicar o layout de URL da extensão faz tudo
|
|
200
|
+
resolver sem tocar no bundle: `npm run sync` termina verificando que as duas fontes
|
|
201
|
+
aterrissaram em `/fonts/`, e falha alto se não.
|
|
202
|
+
|
|
203
|
+
**Consequência de deploy:** o host precisa ficar na **raiz** de um domínio ou
|
|
204
|
+
subdomínio. Para servir sob um subpath, use
|
|
205
|
+
`npm run sync -- --public-path=/embed/` — troca só essa constante no bundle, de forma
|
|
206
|
+
determinística e refeita a cada sync (e aborta se não encontrar exatamente uma
|
|
207
|
+
ocorrência, em vez de adivinhar).
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Desenvolvimento
|
|
212
|
+
|
|
213
|
+
### Pré-requisito: a extensão ao lado
|
|
214
|
+
|
|
215
|
+
Este repositório **não versiona o webphone** — só o SDK e a camada que faz o bundle da
|
|
216
|
+
extensão rodar fora dela. O `popup.js` (932 KB), o `libwebphone.js`, as fontes e os
|
|
217
|
+
`_locales` são copiados da extensão pelo `npm run sync` e ficam fora do git.
|
|
218
|
+
|
|
219
|
+
Clone os dois como irmãos:
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
algum-diretorio/
|
|
223
|
+
├── Bravophone/ ← a extensão (fonte da verdade do webphone)
|
|
224
|
+
└── bravophone-sdk/ ← este repositório
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
git clone https://github.com/teambravotech/bravophone-sdk.git
|
|
229
|
+
cd bravophone-sdk
|
|
230
|
+
npm install
|
|
231
|
+
npm run sync # copia os assets da extensão irmã
|
|
232
|
+
npm run build # gera dist/ (ESM + UMD + sourcemaps)
|
|
233
|
+
npm start # sobe as duas origens de teste
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Se a extensão estiver em outro lugar, passe o caminho:
|
|
237
|
+
`npm run sync -- /caminho/para/Bravophone`.
|
|
238
|
+
|
|
239
|
+
Sem o `sync`, o host não tem o que servir — `npm start` sobe, mas o webphone real não
|
|
240
|
+
carrega (o mock em `?host=mock` continua funcionando).
|
|
241
|
+
|
|
242
|
+
### Scripts
|
|
243
|
+
|
|
244
|
+
| Script | O que faz |
|
|
245
|
+
|---|---|
|
|
246
|
+
| `npm run sync` | Copia os assets da extensão, gera `host/index.html` e `shim/messages.js`, e roda a auditoria de tema |
|
|
247
|
+
| `npm run build` | Gera `dist/` — o que vai para o npm |
|
|
248
|
+
| `npm start` | Sobe as duas origens locais (5173 site, 5174 host) |
|
|
249
|
+
| `npm test` | 100 asserções: shim, geometria da janela e aba de abertura |
|
|
250
|
+
| `npm run audit:theme` | Procura texto invisível no tema escuro |
|
|
251
|
+
|
|
252
|
+
Abra **http://localhost:5173/**.
|
|
253
|
+
|
|
254
|
+
O `npm start` sobe duas portas de propósito — origens diferentes fazem o teste
|
|
255
|
+
exercitar o postMessage cross-origin de verdade, incluindo a validação de origem:
|
|
256
|
+
|
|
257
|
+
| Porta | Papel | Serve |
|
|
258
|
+
|---|---|---|
|
|
259
|
+
| 5173 | site do cliente | `examples/test.html`, carrega `dist/bravophone.umd.cjs` por `<script>`, como no CDN |
|
|
260
|
+
| 5174 | host do webphone | `host/mock.html`, com os headers `frame-ancestors` e `Permissions-Policy` de produção |
|
|
261
|
+
|
|
262
|
+
### Testar sem SIP nem backend
|
|
263
|
+
|
|
264
|
+
O `host/mock.html` carrega **o `chrome-shim.js` e o `guest-bridge.js` reais** e troca
|
|
265
|
+
só o bundle por [`host/mock-webphone.js`](host/mock-webphone.js), que expõe os mesmos
|
|
266
|
+
dois handles que o guest-bridge procura (`window.dialpad` e `window.libwebphone`).
|
|
267
|
+
Ou seja: o caminho testado é o de produção, sem depender de registro SIP.
|
|
268
|
+
|
|
269
|
+
Dá para verificar ponta a ponta o arraste e o resize, a persistência da posição, os
|
|
270
|
+
comandos (`call`/`hangup`/`mute`/`transfer`…), os eventos de volta, uma chamada
|
|
271
|
+
entrante abrindo a janela sozinha, e o `init({ token })` chegando ao storage via shim —
|
|
272
|
+
o painel do mock mostra o `vxToken` gravado.
|
|
273
|
+
|
|
274
|
+
Para testar contra o **webphone real**, rode `npm run sync` e aponte o `hostUrl` do
|
|
275
|
+
`examples/test.html` para `http://localhost:5174/index.html` em vez de `mock.html`.
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
npm test # 18 asserções sobre o chrome-shim, sem browser
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## Deploy
|
|
284
|
+
|
|
285
|
+
### 1. Host — `webphone.bravophone.com` (raiz)
|
|
286
|
+
|
|
287
|
+
Estático (S3+CloudFront, Vercel, nginx). Três headers importam:
|
|
288
|
+
|
|
289
|
+
```
|
|
290
|
+
Content-Security-Policy: frame-ancestors 'self' https://clienteA.com https://clienteB.com;
|
|
291
|
+
Permissions-Policy: microphone=(self)
|
|
292
|
+
Cross-Origin-Opener-Policy: same-origin-allow-popups
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
`frame-ancestors` é o que impede qualquer site de embutir o webphone — deve ser gerado
|
|
296
|
+
a partir de `allowed-origins.json`. É a mesma disciplina de
|
|
297
|
+
[autorização por origem](#) que a extensão já adota; não troque por `*`.
|
|
298
|
+
|
|
299
|
+
Cache: `popup.js`, `js/*`, `fonts/*` com `max-age=31536000` (invalide o CDN a cada
|
|
300
|
+
sync, ou versione por query string); `index.html` sempre com `no-cache`.
|
|
301
|
+
|
|
302
|
+
### 2. Backends — CORS
|
|
303
|
+
|
|
304
|
+
Liberar **uma única origem** em `api.bravophone.com`, `reports.teambravotech.com` e
|
|
305
|
+
`devices.wavoip.com`:
|
|
306
|
+
|
|
307
|
+
```
|
|
308
|
+
Access-Control-Allow-Origin: https://webphone.bravophone.com
|
|
309
|
+
Access-Control-Allow-Credentials: true
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Como o iframe tem origem fixa, essa lista não cresce com o número de clientes.
|
|
313
|
+
|
|
314
|
+
### 3. npm
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
npm publish --access public
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Disponível em `cdn.jsdelivr.net/npm/@bravophone/webphone` e `unpkg.com` logo após.
|
|
321
|
+
Recomende aos integradores a versão travada — `@bravophone/webphone@0.1` — para que
|
|
322
|
+
um major não quebre a página deles.
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## API
|
|
327
|
+
|
|
328
|
+
### `Bravophone.init(options)`
|
|
329
|
+
|
|
330
|
+
| Opção | Tipo | Padrão | Descrição |
|
|
331
|
+
|---|---|---|---|
|
|
332
|
+
| `token` | `string` | — | Token de sessão emitido pelo seu backend |
|
|
333
|
+
| `hostUrl` | `string` | `https://webphone.bravophone.com/embed/` | Origem do webphone |
|
|
334
|
+
| `position` | `string` | `'bottom-right'` | Canto inicial |
|
|
335
|
+
| `open` | `boolean` | `false` | Abrir já visível |
|
|
336
|
+
| `launcher` | `boolean` | `true` | Exibir a aba lateral de abertura |
|
|
337
|
+
| `launcherSide` | `'right' \| 'left'` | `'right'` | Lado em que a aba fica colada |
|
|
338
|
+
| `frame` | `'none' \| 'bar'` | `'none'` | Moldura da janela — ver abaixo |
|
|
339
|
+
| `dockTop` | `'max' \| 'top-half'` | `'max'` | O que arrastar até a borda superior faz |
|
|
340
|
+
| `title` | `string` | `'BRAVOPHONE'` | Texto da barra (só com `frame: 'bar'`) |
|
|
341
|
+
|
|
342
|
+
### Moldura: preservando 100% da UI
|
|
343
|
+
|
|
344
|
+
Por padrão (`frame: 'none'`) **não há barra de título** — a UI do `popup.js` ocupa a
|
|
345
|
+
janela inteira, exatamente como na extensão. Nenhum pixel é tomado.
|
|
346
|
+
|
|
347
|
+
O arraste continua funcionando porque a detecção do gesto acontece **dentro** do
|
|
348
|
+
iframe, no `guest-bridge.js`: o host é cross-origin e não pode tocar naquele DOM, então
|
|
349
|
+
o gesto viaja como delta pela ponte. Qualquer área que não seja botão, campo ou link
|
|
350
|
+
arrasta a janela; o resto continua clicável, e uma seleção de texto em andamento nunca
|
|
351
|
+
é sequestrada. As coordenadas usam `screenX/screenY` — absolutas na tela, imunes ao
|
|
352
|
+
fato de o próprio iframe estar se movendo durante o arraste.
|
|
353
|
+
|
|
354
|
+
Um botão de fechar aparece sobreposto no canto ao passar o mouse, sem empurrar o
|
|
355
|
+
conteúdo. Recolher para o launcher faz o papel de minimizar.
|
|
356
|
+
|
|
357
|
+
Use `frame: 'bar'` se preferir a barra com título, indicador de estado e controles.
|
|
358
|
+
|
|
359
|
+
### Métodos
|
|
360
|
+
|
|
361
|
+
**Janela** — `show()` · `hide()` · `toggle()` · `minimize(force?)` · `move(x, y)` ·
|
|
362
|
+
`resize(w, h)` · `dock(zone)` · `destroy()` · `isOpen` · `geometry`
|
|
363
|
+
|
|
364
|
+
### A aba de abertura
|
|
365
|
+
|
|
366
|
+
Com a janela fechada, o webphone fica acessível por uma **aba colada na lateral** da
|
|
367
|
+
viewport — não um botão circular solto no canto. Ela é arrastável na vertical e
|
|
368
|
+
guarda a posição entre sessões.
|
|
369
|
+
|
|
370
|
+
No repouso mostra só o ícone. No **hover** (ou com foco de teclado) ela expande e
|
|
371
|
+
revela a alça de pontinhos, sinalizando que dá para arrastar.
|
|
372
|
+
|
|
373
|
+
Um detalhe que decide se o componente é agradável ou irritante: **arrastar não abre o
|
|
374
|
+
webphone**. O gesto vira arraste depois de 4px percorridos; abaixo disso continua sendo
|
|
375
|
+
clique. Sem esse limiar, uma tremida de mouse no clique abriria a janela sem querer —
|
|
376
|
+
ou pior, todo arraste terminaria abrindo.
|
|
377
|
+
|
|
378
|
+
A aba também responde a teclado (`Enter` / `Espaço`) e, numa chamada entrante, pulsa em
|
|
379
|
+
vermelho com o contador — visível mesmo com a janela fechada.
|
|
380
|
+
|
|
381
|
+
```js
|
|
382
|
+
Bravophone.init({ launcherSide: 'left' }) // cola do outro lado
|
|
383
|
+
Bravophone.setLauncherSide('right') // troca em runtime
|
|
384
|
+
Bravophone.init({ launcher: false }) // sem aba: você controla com show()
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### Redimensionar e encaixar
|
|
388
|
+
|
|
389
|
+
A janela redimensiona por **qualquer borda ou canto** — as alças laterais são o que
|
|
390
|
+
permite alargar a janela para o histórico de chamadas respirar. O teto é a viewport,
|
|
391
|
+
não um valor fixo.
|
|
392
|
+
|
|
393
|
+
Dois comportamentos de encaixe, ambos com o mesmo vocabulário do Canva:
|
|
394
|
+
|
|
395
|
+
**Arrastando**, encostar numa região da viewport mostra uma prévia azul do encaixe
|
|
396
|
+
antes de soltar:
|
|
397
|
+
|
|
398
|
+
| Onde o cursor chega | Encaixe |
|
|
399
|
+
|---|---|
|
|
400
|
+
| borda esquerda / direita | altura cheia, largura mantida |
|
|
401
|
+
| **borda inferior** | **metade inferior, largura cheia** |
|
|
402
|
+
| borda superior | maximizado (configurável) |
|
|
403
|
+
| os quatro cantos | meia tela esquerda/direita |
|
|
404
|
+
| qualquer outro lugar | segue flutuando |
|
|
405
|
+
|
|
406
|
+
```
|
|
407
|
+
left-half │ max │ right-half
|
|
408
|
+
──────────┼────────────┼──────────
|
|
409
|
+
left │ (flutua) │ right
|
|
410
|
+
──────────┼────────────┼──────────
|
|
411
|
+
left-half │bottom-half │ right-half
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
A borda superior é a única disputada: `max` é o gesto universal (Windows, macOS), mas
|
|
415
|
+
quem trabalha com metades verticais costuma preferir a metade de cima ali. Daí a opção
|
|
416
|
+
`dockTop`:
|
|
417
|
+
|
|
418
|
+
```js
|
|
419
|
+
Bravophone.init({ dockTop: 'top-half' }) // topo encaixa na metade superior
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
Com ela, o máximo continua acessível por `dock('max')`.
|
|
423
|
+
|
|
424
|
+
Arrastar uma janela encaixada de volta para o meio a solta e devolve o tamanho que ela
|
|
425
|
+
tinha antes — e a janela nasce **sob o cursor**, proporcional a onde você a pegou, em
|
|
426
|
+
vez de saltar.
|
|
427
|
+
|
|
428
|
+
**Redimensionando**, chegar a ~32px de uma borda da viewport completa até ela
|
|
429
|
+
sozinha — o "completamento sugestivo".
|
|
430
|
+
|
|
431
|
+
Programaticamente:
|
|
432
|
+
|
|
433
|
+
```js
|
|
434
|
+
Bravophone.dock('right') // altura cheia à direita, largura mantida
|
|
435
|
+
Bravophone.dock('right-half') // metade direita (W/2 × altura cheia)
|
|
436
|
+
Bravophone.dock('bottom-half') // metade inferior (largura cheia × H/2)
|
|
437
|
+
Bravophone.dock('top-half') // metade superior
|
|
438
|
+
Bravophone.dock('bottom') // metade inferior, mantendo a largura atual
|
|
439
|
+
Bravophone.dock('max') // maximiza
|
|
440
|
+
Bravophone.dock('float') // solta e restaura o tamanho anterior
|
|
441
|
+
|
|
442
|
+
Bravophone.on('resize', ({ width, height, dock }) => { /* … */ })
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
O encaixe persiste entre sessões e é **recalculado para a viewport atual** ao recarregar
|
|
446
|
+
— uma janela docada ontem numa tela larga não volta com a geometria de ontem.
|
|
447
|
+
|
|
448
|
+
**Telefonia** — todos retornam `Promise`:
|
|
449
|
+
`call(number, meta?)` · `hangup()` · `answer()` · `mute(on?)` · `hold(on?)` ·
|
|
450
|
+
`sendDTMF(tone)` · `transfer(to)` · `getStatus()` · `setAuth(token)` · `logout()`
|
|
451
|
+
|
|
452
|
+
### Eventos
|
|
453
|
+
|
|
454
|
+
```js
|
|
455
|
+
const off = Bravophone.on('call:incoming', (call) => { /* … */ })
|
|
456
|
+
off() // remove o listener
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
`ready` · `state` · `call:incoming` · `call:answered` · `call:ended` · `call:failed` ·
|
|
460
|
+
`open` · `close` · `error`. Use `'*'` para receber todos como `{ event, payload }`.
|
|
461
|
+
|
|
462
|
+
---
|
|
463
|
+
|
|
464
|
+
## Requisitos
|
|
465
|
+
|
|
466
|
+
- **HTTPS obrigatório** no site do cliente — `getUserMedia` só existe em *secure
|
|
467
|
+
context*. `localhost` funciona no desenvolvimento; o SDK avisa no console se detectar
|
|
468
|
+
contexto inseguro.
|
|
469
|
+
- Navegadores com WebRTC e Shadow DOM: Chrome/Edge 88+, Firefox 90+, Safari 14+.
|
|
470
|
+
- O site do cliente **não** pode ter um CSP `frame-src` que bloqueie
|
|
471
|
+
`webphone.bravophone.com` — vale documentar isso no onboarding.
|
|
472
|
+
|
|
473
|
+
---
|
|
474
|
+
|
|
475
|
+
## Licença
|
|
476
|
+
|
|
477
|
+
Software proprietário da **BravoTech**. Todos os direitos reservados.
|
|
478
|
+
O pacote npm é publicado para consumo pelos integradores; o código deste repositório
|
|
479
|
+
não é open source.
|