@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.
@@ -0,0 +1,340 @@
1
+ (function(W,N){typeof exports=="object"&&typeof module!="undefined"?module.exports=N():typeof define=="function"&&define.amd?define(N):(W=typeof globalThis!="undefined"?globalThis:W||self,W.Bravophone=N())})(this,function(){"use strict";const W='stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"',N=`<svg viewBox="0 0 24 24" fill="none" ${W} aria-hidden="true">
2
+ <path class="bp-i-body" d="M15.6 12.8a11 11 0 0 1-4.4-4.4l1-1a1.6 1.6 0 0 0 .3-1.7c-.2-.7-.4-1.4-.5-2.2A1.6 1.6 0 0 0 10.4 2H7.9a1.6 1.6 0 0 0-1.6 1.8 15.8 15.8 0 0 0 2.4 6.9 15.6 15.6 0 0 0 4.8 4.8 15.8 15.8 0 0 0 6.9 2.4 1.6 1.6 0 0 0 1.7-1.6v-2.5a1.6 1.6 0 0 0-1.4-1.6c-.8-.1-1.5-.3-2.2-.5a1.6 1.6 0 0 0-1.7.4z"/>
3
+ <path class="bp-i-wave bp-i-wave-1" d="M14.5 6.2a4 4 0 0 1 3.3 3.3"/>
4
+ <path class="bp-i-wave bp-i-wave-2" d="M14.1 2.6a7.6 7.6 0 0 1 7.3 7.3"/>
5
+ </svg>`,ue=`<svg viewBox="0 0 24 24" fill="none" ${W} aria-hidden="true">
6
+ <path class="bp-i-bar bp-i-bar-1" d="M4 10v4"/>
7
+ <path class="bp-i-bar bp-i-bar-2" d="M8.7 6.5v11"/>
8
+ <path class="bp-i-bar bp-i-bar-3" d="M12 3.5v17"/>
9
+ <path class="bp-i-bar bp-i-bar-4" d="M15.3 6.5v11"/>
10
+ <path class="bp-i-bar bp-i-bar-5" d="M20 10v4"/>
11
+ </svg>`,ge=`<svg viewBox="0 0 24 24" fill="none" ${W} aria-hidden="true">
12
+ <path class="bp-i-body" d="M4 13v-1a8 8 0 0 1 16 0v1"/>
13
+ <path class="bp-i-body" d="M20 14.5v2a3.5 3.5 0 0 1-3.5 3.5H13"/>
14
+ <rect class="bp-i-cup" x="1.6" y="12.6" width="4.4" height="6.4" rx="1.8"/>
15
+ <rect class="bp-i-cup" x="18" y="12.6" width="4.4" height="6.4" rx="1.8"/>
16
+ </svg>`,fe=`<svg viewBox="0 0 24 24" fill="none" ${W} aria-hidden="true">
17
+ <path class="bp-i-body" d="M21 11.5a8.4 8.4 0 0 1-9 8.4 9 9 0 0 1-2.9-.5L3 21.5l1.5-4.6A8.4 8.4 0 0 1 12 3.1a8.4 8.4 0 0 1 9 8.4z"/>
18
+ <path class="bp-i-hand" d="M14.6 14.4a6.5 6.5 0 0 1-3.2-3.2l.7-.7a1 1 0 0 0 .2-1.1 6 6 0 0 1-.3-1.2 1 1 0 0 0-1-.8H9.6a1 1 0 0 0-1 1.1 9.6 9.6 0 0 0 4.8 7.2 1 1 0 0 0 1.4-.9v-1.2a1 1 0 0 0-.8-1z"/>
19
+ </svg>`,me=`<svg viewBox="0 0 10 16" width="10" height="16" fill="currentColor" aria-hidden="true">
20
+ <circle cx="2.6" cy="2.6" r="1.35"/><circle cx="7.4" cy="2.6" r="1.35"/>
21
+ <circle cx="2.6" cy="8" r="1.35"/><circle cx="7.4" cy="8" r="1.35"/>
22
+ <circle cx="2.6" cy="13.4" r="1.35"/><circle cx="7.4" cy="13.4" r="1.35"/>
23
+ </svg>`,j={"phone-waves":N,waveform:ue,headset:ge,"chat-phone":fe},Z="phone-waves",xe=`
24
+ :host { all: initial; }
25
+ * { box-sizing: border-box; }
26
+
27
+ .bp-root {
28
+ position: fixed;
29
+ z-index: 2147483000;
30
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
31
+ display: flex;
32
+ flex-direction: column;
33
+ background: #1e1e2d;
34
+ border-radius: 12px;
35
+ box-shadow: 0 12px 40px rgba(0,0,0,.45), 0 0 0 1px rgba(255,255,255,.06);
36
+ overflow: hidden;
37
+ will-change: transform;
38
+ }
39
+ .bp-root[hidden] { display: none !important; }
40
+ .bp-root { transition: left .16s ease, top .16s ease, width .16s ease, height .16s ease; }
41
+ .bp-root.bp-dragging,
42
+ .bp-root.bp-resizing { transition: none; user-select: none; }
43
+ .bp-root.bp-minimized .bp-body,
44
+ .bp-root.bp-minimized /* ---- modo sem moldura (frame:'none', padrao) ----
45
+ A UI do webphone ocupa 100% da janela. Os controles ficam sobrepostos,
46
+ invisiveis ate o hover, e nunca empurram nem redimensionam o conteudo. */
47
+ .bp-root.bp-chromeless .bp-header { display: none; }
48
+ .bp-root.bp-chromeless .bp-overlay { display: flex; }
49
+
50
+ .bp-overlay {
51
+ position: absolute;
52
+ top: 6px; right: 6px;
53
+ display: none;
54
+ gap: 2px;
55
+ padding: 3px;
56
+ border-radius: 8px;
57
+ background: rgba(10,10,16,.72);
58
+ backdrop-filter: blur(6px);
59
+ opacity: 0;
60
+ transition: opacity .14s ease;
61
+ z-index: 3;
62
+ pointer-events: none;
63
+ }
64
+ .bp-root.bp-chromeless:hover .bp-overlay,
65
+ .bp-root.bp-chromeless:focus-within .bp-overlay { opacity: 1; pointer-events: auto; }
66
+ .bp-root.bp-dragging .bp-overlay { opacity: 0; pointer-events: none; }
67
+ .bp-overlay .bp-btn { width: 22px; height: 22px; font-size: 13px; color: #cbd5e1; }
68
+
69
+ /* Sem barra de titulo o arraste vem de dentro do iframe: o cursor tambem. */
70
+ .bp-root.bp-chromeless.bp-dragging { cursor: grabbing; }
71
+
72
+ .bp-resize { display: none; }
73
+ .bp-root.bp-minimized { height: auto !important; }
74
+
75
+ .bp-header {
76
+ display: flex;
77
+ align-items: center;
78
+ gap: 8px;
79
+ height: 36px;
80
+ padding: 0 6px 0 12px;
81
+ background: #15151f;
82
+ color: #e6e6f0;
83
+ cursor: grab;
84
+ flex: 0 0 auto;
85
+ touch-action: none;
86
+ }
87
+ .bp-header:active { cursor: grabbing; }
88
+ .bp-title {
89
+ flex: 1 1 auto;
90
+ font-size: 12px;
91
+ font-weight: 600;
92
+ letter-spacing: .3px;
93
+ white-space: nowrap;
94
+ overflow: hidden;
95
+ text-overflow: ellipsis;
96
+ }
97
+ .bp-status {
98
+ width: 8px; height: 8px; border-radius: 50%;
99
+ background: #6b7280; flex: 0 0 auto;
100
+ }
101
+ .bp-status[data-state="ready"] { background: #22c55e; }
102
+ .bp-status[data-state="ringing"] { background: #f59e0b; animation: bp-pulse 1s infinite; }
103
+ .bp-status[data-state="incall"] { background: #22c55e; animation: bp-pulse 2s infinite; }
104
+ .bp-status[data-state="error"] { background: #ef4444; }
105
+ @keyframes bp-pulse { 0%,100% { opacity: 1 } 50% { opacity: .35 } }
106
+
107
+ .bp-btn {
108
+ width: 24px; height: 24px;
109
+ display: grid; place-items: center;
110
+ border: 0; border-radius: 6px;
111
+ background: transparent; color: #9ca3af;
112
+ cursor: pointer; font-size: 15px; line-height: 1;
113
+ flex: 0 0 auto; padding: 0;
114
+ }
115
+ .bp-btn:hover { background: rgba(255,255,255,.08); color: #fff; }
116
+ .bp-btn-close:hover { background: #dc2626; color: #fff; }
117
+
118
+ .bp-body { flex: 1 1 auto; position: relative; min-height: 0; }
119
+ .bp-frame {
120
+ width: 100%; height: 100%;
121
+ border: 0; display: block; background: #1e1e2d;
122
+ }
123
+ /* Durante o arraste o iframe precisa parar de capturar o ponteiro,
124
+ senão o mousemove é engolido pelo documento interno e o drag "trava". */
125
+ .bp-root.bp-dragging .bp-frame,
126
+ .bp-root.bp-resizing .bp-frame { pointer-events: none; }
127
+
128
+ /* ---- alcas de redimensionamento (8 bordas/cantos) ----
129
+ Faixas invisiveis por fora da borda: nao cobrem a UI do webphone e ainda
130
+ assim dao uma area de clique confortavel. */
131
+ .bp-h {
132
+ position: absolute;
133
+ z-index: 4;
134
+ touch-action: none;
135
+ }
136
+ .bp-h-n { top: -4px; left: 10px; right: 10px; height: 8px; cursor: ns-resize; }
137
+ .bp-h-s { bottom: -4px; left: 10px; right: 10px; height: 8px; cursor: ns-resize; }
138
+ .bp-h-w { left: -4px; top: 10px; bottom: 10px; width: 8px; cursor: ew-resize; }
139
+ .bp-h-e { right: -4px; top: 10px; bottom: 10px; width: 8px; cursor: ew-resize; }
140
+ .bp-h-nw { top: -4px; left: -4px; width: 14px; height: 14px; cursor: nwse-resize; }
141
+ .bp-h-ne { top: -4px; right: -4px; width: 14px; height: 14px; cursor: nesw-resize; }
142
+ .bp-h-sw { bottom: -4px; left: -4px; width: 14px; height: 14px; cursor: nesw-resize; }
143
+ .bp-h-se { bottom: -4px; right: -4px; width: 14px; height: 14px; cursor: nwse-resize; }
144
+
145
+ /* Marcador visivel so no canto inferior direito, como affordance. */
146
+ .bp-h-se::after {
147
+ content: ''; position: absolute; right: 3px; bottom: 3px;
148
+ width: 9px; height: 9px;
149
+ background: linear-gradient(135deg, transparent 50%, rgba(255,255,255,.3) 50%);
150
+ }
151
+
152
+ /* Uma janela docada nao se redimensiona pelas bordas coladas na viewport. */
153
+ .bp-root.bp-docked { border-radius: 0; }
154
+
155
+ /* ---- previa de encaixe (docking) ----
156
+ Fantasma que mostra onde a janela vai parar antes de soltar. */
157
+ .bp-preview {
158
+ position: fixed;
159
+ z-index: 2147482998;
160
+ border-radius: 10px;
161
+ background: rgba(37,99,235,.18);
162
+ border: 2px solid rgba(96,165,250,.85);
163
+ box-shadow: 0 0 0 1px rgba(0,0,0,.25), inset 0 0 40px rgba(96,165,250,.15);
164
+ pointer-events: none;
165
+ transition: left .12s ease, top .12s ease, width .12s ease, height .12s ease;
166
+ }
167
+ .bp-preview[hidden] { display: none !important; }
168
+
169
+ .bp-launcher {
170
+ position: fixed;
171
+ top: 50%;
172
+ z-index: 2147482999;
173
+ display: flex;
174
+ align-items: center;
175
+ gap: 0;
176
+ height: 48px;
177
+ padding: 0 11px;
178
+ background: #6c5ce7;
179
+ color: #fff;
180
+ border: 0;
181
+ cursor: pointer;
182
+ box-shadow: 0 4px 16px rgba(76,58,190,.42), 0 0 0 1px rgba(255,255,255,.08) inset;
183
+ transition: padding .16s ease, background .16s ease, box-shadow .16s ease;
184
+ touch-action: none;
185
+ user-select: none;
186
+ -webkit-user-select: none;
187
+ }
188
+ .bp-launcher[hidden] { display: none !important; }
189
+
190
+ /* Colada na borda: os cantos arredondados sao so os que ficam para dentro. */
191
+ .bp-launcher[data-side="right"] { border-radius: 14px 0 0 14px; }
192
+ .bp-launcher[data-side="left"] { border-radius: 0 14px 14px 0; }
193
+
194
+ .bp-launcher:hover,
195
+ .bp-launcher:focus-visible {
196
+ background: #5b4bd6;
197
+ box-shadow: 0 6px 22px rgba(76,58,190,.55), 0 0 0 1px rgba(255,255,255,.12) inset;
198
+ }
199
+ .bp-launcher:focus-visible { outline: 2px solid #c7d2fe; outline-offset: 2px; }
200
+ .bp-launcher.bp-launcher-dragging { cursor: grabbing; transition: none; }
201
+
202
+ /* Alca de arraste: os pontos ficam sempre com largura fixa e somem por
203
+ opacidade + margem negativa. Animar a largura cortaria o SVG no meio. */
204
+ .bp-launcher-grip {
205
+ width: 10px;
206
+ height: 16px;
207
+ flex: 0 0 auto;
208
+ opacity: 0;
209
+ color: rgba(255,255,255,.9);
210
+ cursor: grab;
211
+ transition: opacity .16s ease, margin .16s ease;
212
+ }
213
+ .bp-launcher-grip svg { display: block; }
214
+ .bp-launcher[data-side="right"] .bp-launcher-grip { order: 0; margin-right: -10px; }
215
+ .bp-launcher[data-side="left"] .bp-launcher-grip { order: 2; margin-left: -10px; }
216
+
217
+ .bp-launcher:hover .bp-launcher-grip,
218
+ .bp-launcher:focus-visible .bp-launcher-grip,
219
+ .bp-launcher.bp-launcher-dragging .bp-launcher-grip { opacity: 1; }
220
+ .bp-launcher[data-side="right"]:hover .bp-launcher-grip,
221
+ .bp-launcher[data-side="right"]:focus-visible .bp-launcher-grip,
222
+ .bp-launcher[data-side="right"].bp-launcher-dragging .bp-launcher-grip { margin-right: 7px; }
223
+ .bp-launcher[data-side="left"]:hover .bp-launcher-grip,
224
+ .bp-launcher[data-side="left"]:focus-visible .bp-launcher-grip,
225
+ .bp-launcher[data-side="left"].bp-launcher-dragging .bp-launcher-grip { margin-left: 7px; }
226
+
227
+ .bp-launcher-icon {
228
+ position: relative;
229
+ order: 1;
230
+ display: grid;
231
+ place-items: center;
232
+ width: 22px;
233
+ height: 22px;
234
+ flex: 0 0 auto;
235
+ pointer-events: none;
236
+ }
237
+ .bp-launcher-icon svg { width: 100%; height: 100%; display: block; }
238
+
239
+ .bp-launcher .bp-badge {
240
+ position: absolute;
241
+ top: -5px;
242
+ min-width: 18px;
243
+ height: 18px;
244
+ padding: 0 5px;
245
+ border-radius: 9px;
246
+ background: #ef4444;
247
+ color: #fff;
248
+ font: 700 11px/18px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
249
+ text-align: center;
250
+ display: none;
251
+ box-shadow: 0 0 0 2px #6c5ce7;
252
+ }
253
+ .bp-launcher[data-side="right"] .bp-badge { left: -6px; }
254
+ .bp-launcher[data-side="left"] .bp-badge { right: -6px; }
255
+ .bp-launcher[data-badge]:not([data-badge=""]) .bp-badge { display: block; }
256
+
257
+ /* Chamada entrante: a aba pulsa para chamar atencao mesmo fechada. */
258
+ @keyframes bp-launcher-ring {
259
+ 0%, 100% { box-shadow: 0 4px 16px rgba(76,58,190,.42); }
260
+ 50% { box-shadow: 0 4px 16px rgba(239,68,68,.75), 0 0 0 4px rgba(239,68,68,.25); }
261
+ }
262
+ .bp-launcher[data-badge]:not([data-badge=""]) { animation: bp-launcher-ring 1.1s infinite; }
263
+
264
+ @media (prefers-reduced-motion: reduce) {
265
+ .bp-launcher, .bp-launcher-grip { transition: none; }
266
+ .bp-launcher[data-badge]:not([data-badge=""]) { animation: none; }
267
+ }
268
+
269
+
270
+ .bp-launcher-icon svg { width: 100%; height: 100%; display: block; overflow: visible; }
271
+
272
+ /* Ponto de status no canto do ícone — verde registrado, cinza sem linha. */
273
+ .bp-launcher-icon::after {
274
+ content: '';
275
+ position: absolute;
276
+ right: -1px; bottom: -1px;
277
+ width: 7px; height: 7px;
278
+ border-radius: 50%;
279
+ background: #94a3b8;
280
+ box-shadow: 0 0 0 2px #6c5ce7;
281
+ transition: background .2s ease;
282
+ }
283
+ .bp-launcher[data-state="ready"] .bp-launcher-icon::after { background: #22c55e; }
284
+ .bp-launcher[data-state="error"] .bp-launcher-icon::after { background: #ef4444; }
285
+ /* Tocando ou em chamada, o ponto sai: a animacao ja comunica, e ele sujaria. */
286
+ .bp-launcher[data-state="ringing"] .bp-launcher-icon::after,
287
+ .bp-launcher[data-state="incall"] .bp-launcher-icon::after { display: none; }
288
+
289
+ /* --- ondas do handset: crescem de dentro para fora --- */
290
+ .bp-i-wave { opacity: .45; transform-origin: 12px 12px; }
291
+ .bp-launcher[data-state="ready"] .bp-i-wave { opacity: .75; }
292
+ @keyframes bp-i-emit {
293
+ 0% { opacity: 0; transform: scale(.6); }
294
+ 40% { opacity: 1; }
295
+ 100% { opacity: 0; transform: scale(1.15); }
296
+ }
297
+ .bp-launcher[data-state="ringing"] .bp-i-wave,
298
+ .bp-launcher[data-state="incall"] .bp-i-wave {
299
+ animation: bp-i-emit 1.4s ease-out infinite;
300
+ }
301
+ .bp-launcher[data-state="ringing"] .bp-i-wave-2,
302
+ .bp-launcher[data-state="incall"] .bp-i-wave-2 { animation-delay: .35s; }
303
+
304
+ /* --- handset balanca quando toca, como um telefone de verdade --- */
305
+ @keyframes bp-i-shake {
306
+ 0%, 100% { transform: rotate(0deg); }
307
+ 15%, 45% { transform: rotate(-13deg); }
308
+ 30%, 60% { transform: rotate(13deg); }
309
+ 75% { transform: rotate(0deg); }
310
+ }
311
+ .bp-launcher[data-state="ringing"] .bp-i-body,
312
+ .bp-launcher[data-state="ringing"] .bp-i-hand {
313
+ transform-origin: 12px 12px;
314
+ animation: bp-i-shake 1s ease-in-out infinite;
315
+ }
316
+
317
+ /* --- equalizador: barras dancam so em chamada --- */
318
+ .bp-i-bar { transform-origin: center; transition: transform .25s ease; }
319
+ .bp-launcher[data-state="incall"] .bp-i-bar {
320
+ animation: bp-i-dance .9s ease-in-out infinite;
321
+ }
322
+ @keyframes bp-i-dance {
323
+ 0%, 100% { transform: scaleY(.45); }
324
+ 50% { transform: scaleY(1); }
325
+ }
326
+ .bp-launcher[data-state="incall"] .bp-i-bar-2 { animation-delay: .12s }
327
+ .bp-launcher[data-state="incall"] .bp-i-bar-3 { animation-delay: .24s }
328
+ .bp-launcher[data-state="incall"] .bp-i-bar-4 { animation-delay: .36s }
329
+ .bp-launcher[data-state="incall"] .bp-i-bar-5 { animation-delay: .48s }
330
+
331
+ /* Hover: o icone da uma leve subida, so para o botao parecer vivo. */
332
+ .bp-launcher:hover .bp-launcher-icon { transform: translateY(-1px); }
333
+ .bp-launcher-icon { transition: transform .16s ease; }
334
+
335
+ @media (prefers-reduced-motion: reduce) {
336
+ .bp-i-wave, .bp-i-body, .bp-i-hand, .bp-i-bar { animation: none !important; }
337
+ .bp-launcher-icon { transition: none; }
338
+ }
339
+
340
+ `,ie="bravophone:widget:geometry",q=28,J=32;function we(e){try{const a=localStorage.getItem(ie);if(!a)return{...e};const i=JSON.parse(a);return i.dock?{...e,...i,...U(i.dock,i)}:{...e,...i}}catch(a){return{...e}}}function X(e){try{localStorage.setItem(ie,JSON.stringify(e))}catch(a){}}function re(e){return{minWidth:e.minWidth,minHeight:e.minHeight,maxWidth:Math.max(e.minWidth,Math.min(e.maxWidth,window.innerWidth)),maxHeight:Math.max(e.minHeight,Math.min(e.maxHeight,window.innerHeight))}}function O(e){const a=Math.max(0,window.innerWidth-e.width),i=Math.max(0,window.innerHeight-40);return{...e,x:Math.min(Math.max(0,e.x),a),y:Math.min(Math.max(0,e.y),i)}}function U(e,a){const i=window.innerWidth,r=window.innerHeight,o=Math.round(i/2),s=Math.round(r/2),u=Math.min(a.width,i);switch(e){case"left":return{x:0,y:0,width:u,height:r};case"right":return{x:i-u,y:0,width:u,height:r};case"left-half":return{x:0,y:0,width:o,height:r};case"right-half":return{x:i-o,y:0,width:o,height:r};case"top":return{x:a.x,y:0,width:u,height:s};case"bottom":return{x:a.x,y:r-s,width:u,height:s};case"top-half":return{x:0,y:0,width:i,height:s};case"bottom-half":return{x:0,y:r-s,width:i,height:s};case"max":return{x:0,y:0,width:i,height:r};default:return{x:a.x,y:a.y,width:a.width,height:a.height}}}function ye(e,a,i){const r=window.innerWidth,o=window.innerHeight,s=e<=q,u=e>=r-q,x=a<=q,t=a>=o-q;return(x||t)&&s?"left-half":(x||t)&&u?"right-half":x?i||"max":t?"bottom-half":s?"left":u?"right":null}function ve({root:e,handle:a,handles:i,preview:r,geometry:o,limits:s,topDock:u,onChange:x}){let t={...o},g=null,p="",d=null,h=null,y=null,v=null;const z=()=>{e.style.left=`${t.x}px`,e.style.top=`${t.y}px`,e.style.width=`${t.width}px`,e.style.height=`${t.height}px`,e.classList.toggle("bp-docked",!!t.dock),x==null||x(t)},A=n=>{if(r){if(!n){r.hidden=!0;return}r.hidden=!1,r.style.left=`${n.x}px`,r.style.top=`${n.y}px`,r.style.width=`${n.width}px`,r.style.height=`${n.height}px`}},$=(n,b)=>{const l=re(s);let{x:E,y:f,width:k,height:H}=d;p.includes("e")&&(k=d.width+n),p.includes("w")&&(k=d.width-n),p.includes("s")&&(H=d.height+b),p.includes("n")&&(H=d.height-b),k=Math.min(l.maxWidth,Math.max(l.minWidth,k)),H=Math.min(l.maxHeight,Math.max(l.minHeight,H)),p.includes("w")&&(E=d.x+(d.width-k)),p.includes("n")&&(f=d.y+(d.height-H)),p.includes("e")&&Math.abs(E+k-window.innerWidth)<=J&&(k=window.innerWidth-E),p.includes("w")&&Math.abs(E)<=J&&(k+=E,E=0),p.includes("s")&&Math.abs(f+H-window.innerHeight)<=J&&(H=window.innerHeight-f),p.includes("n")&&Math.abs(f)<=J&&(H+=f,f=0),t={...t,x:E,y:f,width:k,height:H,dock:null},z()},R=(n,b,l,E)=>{if(t.dock){const f=v||{width:s.width,height:s.height},k=h.x/Math.max(1,t.width);t={...t,...f,dock:null},h={x:Math.round(f.width*k),y:Math.min(h.y,32)},d={...d,x:l-h.x,y:E-h.y,px:d.px+n,py:d.py+b},n=0,b=0}t=O({...t,x:d.x+n,y:d.y+b}),z(),y=ye(l,E,u),A(y?U(y,t):null)},P=()=>{y&&(v=v||{width:t.width,height:t.height},t={...t,...U(y,t),dock:y},z())},F=(n,b)=>l=>{l.button===0&&(n==="drag"&&l.target.closest(".bp-btn")||(g=n,p=b||"",d={px:l.clientX,py:l.clientY,...t},h={x:l.clientX-t.x,y:l.clientY-t.y},t.dock||(v={width:t.width,height:t.height}),e.classList.add(n==="drag"?"bp-dragging":"bp-resizing"),l.currentTarget.setPointerCapture(l.pointerId),l.preventDefault()))},Y=n=>{if(!g||!d)return;const b=n.clientX-d.px,l=n.clientY-d.py;g==="drag"?R(b,l,n.clientX,n.clientY):$(b,l)},D=n=>{if(g){e.classList.remove("bp-dragging","bp-resizing"),A(null),g==="drag"&&P(),g=null,p="",d=null,y=null;try{n.currentTarget.releasePointerCapture(n.pointerId)}catch(b){}X(t)}};a.addEventListener("pointerdown",F("drag")),a.addEventListener("pointermove",Y),a.addEventListener("pointerup",D),a.addEventListener("pointercancel",D),(i||[]).forEach(({el:n,dir:b})=>{n.addEventListener("pointerdown",F("resize",b)),n.addEventListener("pointermove",Y),n.addEventListener("pointerup",D),n.addEventListener("pointercancel",D)});const ae=()=>{t=t.dock?{...t,...U(t.dock,t)}:O(t),z()};window.addEventListener("resize",ae);const K={start(n){var b,l;g="drag",d={px:n.x,py:n.y,...t},h={x:(b=n.cx)!=null?b:t.width/2,y:(l=n.cy)!=null?l:20},t.dock||(v={width:t.width,height:t.height}),e.classList.add("bp-dragging")},move(n){if(g!=="drag"||!d)return;const b=n.x-d.px,l=n.y-d.py;R(b,l,d.x+b+h.x,d.y+l+h.y)},end(){g==="drag"&&(e.classList.remove("bp-dragging"),A(null),P(),g=null,d=null,y=null,X(t))}};return z(),{external:K,get geometry(){return{...t}},set(n){const b=re(s),l={...t,...n,dock:null};l.width=Math.min(b.maxWidth,Math.max(b.minWidth,l.width)),l.height=Math.min(b.maxHeight,Math.max(b.minHeight,l.height)),t=O(l),z(),X(t)},dock(n){if(!n||n==="float"){const b=v||{width:s.width,height:s.height};t=O({...t,...b,dock:null})}else t.dock||(v={width:t.width,height:t.height}),t={...t,...U(n,t),dock:n};z(),X(t)},destroy(){window.removeEventListener("resize",ae)}}}const oe="bravophone/v1";function ke({frame:e,origin:a,onEvent:i}){const r=new Map;let o=0,s=!1;const u=[],x=g=>{var d;if(g.origin!==a||g.source!==e.contentWindow)return;const p=g.data;if(!(!p||p.protocol!==oe)){if(p.type==="ready"){for(s=!0;u.length;)u.shift()();i==null||i("ready",(d=p.payload)!=null?d:{});return}if(p.type==="reply"){const h=r.get(p.id);if(!h)return;r.delete(p.id),clearTimeout(h.timer),p.error?h.reject(new Error(p.error)):h.resolve(p.payload);return}p.type==="event"&&(i==null||i(p.name,p.payload))}};window.addEventListener("message",x);function t(g,p,{timeout:d=15e3}={}){return new Promise((h,y)=>{const v=`${Date.now()}-${++o}`,z=()=>{var $;const A=setTimeout(()=>{r.delete(v),y(new Error(`Bravophone: timeout no comando "${g}"`))},d);r.set(v,{resolve:h,reject:y,timer:A}),($=e.contentWindow)==null||$.postMessage({protocol:oe,type:"command",id:v,command:g,payload:p},a)};s?z():u.push(z)})}return{call:t,get ready(){return s},destroy(){window.removeEventListener("message",x),r.forEach(({reject:g,timer:p})=>{clearTimeout(p),g(new Error("Bravophone: widget destruído"))}),r.clear()}}}const se="bravophone:launcher",Me=4,Q=8;function ze(e){try{const a=localStorage.getItem(se);return a?{...e,...JSON.parse(a)}:{...e}}catch(a){return{...e}}}function ee(e){try{localStorage.setItem(se,JSON.stringify(e))}catch(a){}}function _(e,a){const i=Math.max(Q,window.innerHeight-a-Q);return Math.min(Math.max(Q,e),i)}function Le({el:e,side:a,y:i,onOpen:r}){let o={side:a,y:_(i,e.offsetHeight||48)},s=null,u=!1;const x=()=>{e.dataset.side=o.side,e.style.top=`${o.y}px`,e.style.right=o.side==="right"?"0px":"auto",e.style.left=o.side==="left"?"0px":"auto"},t=h=>{h.button===0&&(s={py:h.clientY,y:o.y},u=!1,e.setPointerCapture(h.pointerId))},g=h=>{if(!s)return;const y=h.clientY-s.py;!u&&Math.abs(y)<Me||(u||(u=!0,e.classList.add("bp-launcher-dragging")),o.y=_(s.y+y,e.offsetHeight),x())},p=h=>{if(!s)return;const y=u;s=null,u=!1,e.classList.remove("bp-launcher-dragging");try{e.releasePointerCapture(h.pointerId)}catch(v){}y?ee(o):r==null||r()};e.addEventListener("pointerdown",t),e.addEventListener("pointermove",g),e.addEventListener("pointerup",p),e.addEventListener("pointercancel",p),e.addEventListener("keydown",h=>{(h.key==="Enter"||h.key===" ")&&(h.preventDefault(),r==null||r())});const d=()=>{o.y=_(o.y,e.offsetHeight),x()};return window.addEventListener("resize",d),x(),{get state(){return{...o}},setSide(h){o.side=h,x(),ee(o)},moveTo(h){o.y=_(h,e.offsetHeight),x(),ee(o)},destroy(){window.removeEventListener("resize",d)}}}const de={width:380,height:640,minWidth:320,minHeight:420,maxWidth:1600,maxHeight:1400,margin:24};function w(e,a={},i=[]){const r=document.createElement(e);for(const[o,s]of Object.entries(a))o==="class"?r.className=s:o==="text"?r.textContent=s:s!=null&&r.setAttribute(o,s);return i.forEach(o=>r.appendChild(o)),r}function Se(e){const{hostUrl:a,token:i,position:r="bottom-right",open:o=!1,launcher:s=!0,title:u="BRAVOPHONE",frame:x="none",dockTop:t="max",launcherSide:g="right",launcherIcon:p=Z,emit:d}=e,h=new URL(a),y=h.origin,v=w("div",{"data-bravophone":"root"});v.style.cssText="all:initial;position:static";const z=v.attachShadow({mode:"open"});z.appendChild(w("style",{text:xe}));const A=(()=>{var be;const{width:c,height:S,margin:I}=de,G=window.innerWidth-c-I,he=window.innerHeight-S-I,le={"bottom-right":{x:G,y:he},"bottom-left":{x:I,y:he},"top-right":{x:G,y:I},"top-left":{x:I,y:I}};return O({...(be=le[r])!=null?be:le["bottom-right"],width:c,height:S})})(),$=O(we(A)),R=w("span",{class:"bp-status","data-state":"connecting"}),P=w("button",{class:"bp-btn bp-btn-min",title:"Minimizar",text:"—"}),F=w("button",{class:"bp-btn bp-btn-close",title:"Fechar",text:"×"}),Y=w("div",{class:"bp-header"},[R,w("span",{class:"bp-title",text:u}),P,F]),D=w("iframe",{class:"bp-frame",src:Ee(h,{token:i,embed:"1",parent:location.origin}),allow:"microphone; autoplay; clipboard-write; speaker-selection",title:"Webphone BRAVOPHONE"}),K=["n","s","w","e","nw","ne","sw","se"].map(c=>({el:w("div",{class:`bp-h bp-h-${c}`}),dir:c})),n=w("div",{class:"bp-preview"});n.hidden=!0;const b=w("button",{class:"bp-btn bp-btn-close",title:"Fechar",text:"×"}),l=w("div",{class:"bp-overlay"},[b]),E=w("div",{class:"bp-body"},[D,l]),f=w("div",{class:"bp-root"},[Y,E,...K.map(c=>c.el)]);x!=="bar"&&f.classList.add("bp-chromeless"),f.hidden=!o;const k=w("span",{class:"bp-launcher-icon"});k.innerHTML=j[p]||j[Z];const H=w("span",{class:"bp-launcher-grip","aria-hidden":"true"});H.innerHTML=me;const L=w("div",{class:"bp-launcher",role:"button",tabindex:"0",title:"Abrir webphone","aria-label":"Abrir webphone"},[H,k,w("span",{class:"bp-badge"})]);L.dataset.state="connecting",L.hidden=!s||o,z.append(n,f,L),document.body.appendChild(v);const ce=ze({side:g,y:Math.round(window.innerHeight/2-24)}),pe=Le({el:L,side:ce.side,y:_(ce.y,48),onOpen:()=>T.show()}),C=ve({root:f,handle:Y,handles:K,preview:n,geometry:$,limits:de,topDock:t,onChange:c=>d("resize",{width:c.width,height:c.height,dock:c.dock||null})}),ne=ke({frame:D,origin:y,onEvent:(c,S)=>{var I;if(c==="ready"&&(R.dataset.state="ready",L.dataset.state="ready",i&&ne.call("auth",{token:i}).catch(()=>{})),c==="state"){const G=(I=S==null?void 0:S.state)!=null?I:"ready";R.dataset.state=G,L.dataset.state=G}if(c==="call:incoming"&&(T.show(),L.dataset.badge="1",L.dataset.state="ringing"),c==="call:answered"&&(L.dataset.state="incall"),(c==="call:ended"||c==="call:failed")&&(delete L.dataset.badge,L.dataset.state="ready"),c==="drag:start"){C.external.start(S);return}if(c==="drag:move"){C.external.move(S);return}if(c==="drag:end"){C.external.end();return}d(c,S)}});let V=!1;const T={el:v,bridge:ne,show(){f.hidden=!1,L.hidden=!0,d("open")},hide(){f.hidden=!0,s&&(L.hidden=!1),d("close")},toggle(){f.hidden?T.show():T.hide()},get isOpen(){return!f.hidden},minimize(c){if(x!=="bar"){T.hide();return}V=c!=null?c:!V,f.classList.toggle("bp-minimized",V),P.textContent=V?"▢":"—"},move(c,S){C.set({x:c,y:S})},resize(c,S){C.set({width:c,height:S})},dock(c){C.dock(c)},setLauncherSide(c){pe.setSide(c)},setLauncherIcon(c){k.innerHTML=j[c]||j[Z]},get geometry(){return C.geometry},destroy(){ne.destroy(),C.destroy(),pe.destroy(),n.remove(),v.remove()}};return F.addEventListener("click",()=>T.hide()),P.addEventListener("click",()=>T.minimize()),b.addEventListener("click",()=>T.hide()),T}function Ee(e,a){const i=new URL(e.href);for(const[r,o]of Object.entries(a))o!=null&&o!==""&&i.searchParams.set(r,o);return a.token&&(i.searchParams.delete("token"),i.hash=`token=${encodeURIComponent(a.token)}`),i.href}const He="https://webphone.bravophone.com/embed/";let M=null;const B=new Map;function Te(e,a){var i,r;(i=B.get(e))==null||i.forEach(o=>{try{o(a)}catch(s){console.error("[Bravophone] listener falhou:",s)}}),(r=B.get("*"))==null||r.forEach(o=>{try{o({event:e,payload:a})}catch(s){console.error("[Bravophone] listener falhou:",s)}})}function m(){if(!M)throw new Error("Bravophone: chame Bravophone.init() antes.");return M}const te={init(e={}){if(M)return M;if(typeof window=="undefined"||!document.body)throw new Error("Bravophone: init() precisa rodar no browser, após o <body> existir.");return location.protocol!=="https:"&&location.hostname!=="localhost"&&console.warn("[Bravophone] Contexto inseguro: o microfone exige HTTPS ou localhost."),M=Se({hostUrl:He,...e,emit:Te}),M},show(){m().show()},hide(){m().hide()},toggle(){m().toggle()},minimize(e){m().minimize(e)},move(e,a){m().move(e,a)},resize(e,a){m().resize(e,a)},dock(e){m().dock(e)},setLauncherSide(e){m().setLauncherSide(e)},setLauncherIcon(e){m().setLauncherIcon(e)},get isOpen(){return M?M.isOpen:!1},get geometry(){return M?M.geometry:null},call(e,a){return m().bridge.call("call",{number:e,meta:a})},hangup(){return m().bridge.call("hangup")},answer(){return m().bridge.call("answer")},mute(){return m().bridge.call("mute")},hold(){return m().bridge.call("hold")},sendDTMF(e){return m().bridge.call("dtmf",{tone:e})},transfer(e){return m().bridge.call("transfer",{to:e})},getStatus(){return m().bridge.call("status")},setAuth(e){return m().bridge.call("auth",{token:e})},logout(){return m().bridge.call("logout")},on(e,a){return B.has(e)||B.set(e,new Set),B.get(e).add(a),()=>te.off(e,a)},off(e,a){var i;(i=B.get(e))==null||i.delete(a)},destroy(){M==null||M.destroy(),M=null,B.clear()},version:"0.1.0"};return typeof window!="undefined"&&(window.Bravophone=te),te});
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@bravophone/webphone",
3
+ "version": "0.1.0",
4
+ "description": "Webphone BRAVOPHONE embutível em qualquer página web. Renderiza uma janela flutuante arrastável com todas as funcionalidades do softphone.",
5
+ "type": "module",
6
+ "main": "./dist/bravophone.umd.cjs",
7
+ "module": "./dist/bravophone.js",
8
+ "types": "./types/index.d.ts",
9
+ "unpkg": "./dist/bravophone.umd.cjs",
10
+ "jsdelivr": "./dist/bravophone.umd.cjs",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./types/index.d.ts",
14
+ "import": "./dist/bravophone.js",
15
+ "require": "./dist/bravophone.umd.cjs"
16
+ },
17
+ "./style.css": "./dist/bravophone.css"
18
+ },
19
+ "files": [
20
+ "dist/*.js",
21
+ "dist/*.cjs",
22
+ "types",
23
+ "README.md"
24
+ ],
25
+ "sideEffects": false,
26
+ "scripts": {
27
+ "sync": "node scripts/sync-from-extension.mjs && node scripts/audit-theme.mjs",
28
+ "dev": "node scripts/dev-server.mjs",
29
+ "build": "vite build",
30
+ "build:host": "npm run sync && vite build --config vite.host.config.js",
31
+ "test": "node scripts/smoke-shim.mjs && node scripts/smoke-geometry.mjs && node scripts/smoke-launcher.mjs",
32
+ "start": "node scripts/dev-server.mjs",
33
+ "audit:theme": "node scripts/audit-theme.mjs",
34
+ "prepublishOnly": "npm run build && npm test"
35
+ },
36
+ "keywords": [
37
+ "webphone",
38
+ "softphone",
39
+ "sip",
40
+ "webrtc",
41
+ "voip",
42
+ "bravophone",
43
+ "widget"
44
+ ],
45
+ "license": "UNLICENSED",
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "devDependencies": {
50
+ "vite": "^5.4.0"
51
+ },
52
+ "author": "BravoTech",
53
+ "homepage": "https://github.com/teambravotech/bravophone-sdk#readme",
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "git+https://github.com/teambravotech/bravophone-sdk.git"
57
+ },
58
+ "bugs": {
59
+ "url": "https://github.com/teambravotech/bravophone-sdk/issues"
60
+ },
61
+ "engines": {
62
+ "node": ">=18"
63
+ },
64
+ "browser": "./dist/bravophone.umd.cjs"
65
+ }
@@ -0,0 +1,153 @@
1
+ export type LauncherIcon = 'phone-waves' | 'waveform' | 'headset' | 'chat-phone'
2
+
3
+ export interface BravophoneOptions {
4
+ /** Origem do webphone hospedado. Padrão: https://webphone.bravophone.com/embed/ */
5
+ hostUrl?: string
6
+ /** Token de sessão do usuário, emitido pelo backend do integrador. */
7
+ token?: string
8
+ /** Canto inicial da janela. Padrão: 'bottom-right'. */
9
+ position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'
10
+ /** Abrir já visível. Padrão: false (mostra só o botão flutuante). */
11
+ open?: boolean
12
+ /** Exibir a aba lateral de abertura. Padrão: true. */
13
+ launcher?: boolean
14
+ /** Lado da viewport em que a aba fica colada. Padrão: 'right'. */
15
+ launcherSide?: 'right' | 'left'
16
+ /** Desenho da aba. Padrão: 'phone-waves'. */
17
+ launcherIcon?: LauncherIcon
18
+ /** Texto da barra de título. Só usado quando `frame: 'bar'`. */
19
+ title?: string
20
+ /**
21
+ * Moldura da janela. Padrão: `'none'`.
22
+ * - `'none'`: sem barra de título — a UI do webphone ocupa 100% da janela e o
23
+ * arraste acontece pelo próprio app. Só um botão de fechar no hover.
24
+ * - `'bar'`: adiciona barra com título, indicador de estado, minimizar e fechar.
25
+ */
26
+ frame?: 'none' | 'bar'
27
+ /**
28
+ * O que arrastar até a borda superior faz. Padrão: `'max'` (gesto universal).
29
+ * Use `'top-half'` para que o topo encaixe na metade superior — o máximo
30
+ * continua disponível por `dock('max')`.
31
+ */
32
+ dockTop?: 'max' | 'top-half'
33
+ }
34
+
35
+ export interface CallInfo {
36
+ id: string | null
37
+ number: string | null
38
+ direction: 'inbound' | 'outbound' | null
39
+ }
40
+
41
+ export interface PhoneStatus {
42
+ /** O app terminou de montar e aceita comandos. */
43
+ ready: boolean
44
+ inCall: boolean
45
+ /** Fase bruta do bundle (dialing, ringing, answered…), quando disponível. */
46
+ phase: string | null
47
+ number: string | null
48
+ incoming: boolean
49
+ muted: boolean
50
+ held: boolean
51
+ }
52
+
53
+ export type DockZone =
54
+ /** Altura cheia, colada numa lateral; a largura atual é mantida. */
55
+ | 'left' | 'right'
56
+ /** Metade da tela na horizontal: largura W/2, altura cheia. */
57
+ | 'left-half' | 'right-half'
58
+ /** Largura cheia, metade da tela na vertical: altura H/2. */
59
+ | 'top-half' | 'bottom-half'
60
+ /** Metade da altura, mantendo a largura atual. */
61
+ | 'top' | 'bottom'
62
+ | 'max'
63
+ /** Solta o encaixe e devolve o tamanho flutuante anterior. */
64
+ | 'float'
65
+
66
+ export interface Geometry {
67
+ x: number
68
+ y: number
69
+ width: number
70
+ height: number
71
+ /** Zona encaixada, ou null quando a janela está flutuando. */
72
+ dock: Exclude<DockZone, 'float'> | null
73
+ }
74
+
75
+ export interface BravophoneEvents {
76
+ ready: { version: string }
77
+ state: { state: 'connecting' | 'ready' | 'ringing' | 'incall' | 'error' }
78
+ 'call:incoming': CallInfo
79
+ 'call:dialing': CallInfo
80
+ 'call:answered': CallInfo
81
+ 'call:ended': CallInfo
82
+ 'call:failed': CallInfo & { reason?: string }
83
+ resize: { width: number; height: number; dock: Exclude<DockZone, 'float'> | null }
84
+ open: undefined
85
+ close: undefined
86
+ error: { message: string }
87
+ }
88
+
89
+ export interface BravophoneInstance {
90
+ el: HTMLElement
91
+ show(): void
92
+ hide(): void
93
+ toggle(): void
94
+ readonly isOpen: boolean
95
+ minimize(force?: boolean): void
96
+ move(x: number, y: number): void
97
+ resize(width: number, height: number): void
98
+ dock(zone: DockZone): void
99
+ setLauncherSide(side: 'right' | 'left'): void
100
+ setLauncherIcon(name: LauncherIcon): void
101
+ readonly geometry: Geometry
102
+ destroy(): void
103
+ }
104
+
105
+ export interface BravophoneAPI {
106
+ init(options?: BravophoneOptions): BravophoneInstance
107
+
108
+ show(): void
109
+ hide(): void
110
+ toggle(): void
111
+ minimize(force?: boolean): void
112
+ move(x: number, y: number): void
113
+ resize(width: number, height: number): void
114
+ /** Encaixa a janela numa zona da viewport, ou 'float' para soltar. */
115
+ dock(zone: DockZone): void
116
+ readonly isOpen: boolean
117
+ readonly geometry: Geometry | null
118
+
119
+ /** Disca um número. Formato livre; a normalização é a mesma da extensão. */
120
+ call(number: string, meta?: Record<string, unknown>): Promise<{ ok: true }>
121
+ hangup(): Promise<{ ok: true }>
122
+ answer(): Promise<{ ok: true }>
123
+ /** Alterna o mudo. O bundle só expõe toggle — não aceita estado alvo. */
124
+ mute(): Promise<{ ok: true }>
125
+ /** Alterna a espera. Também é toggle, não set. */
126
+ hold(): Promise<{ ok: true }>
127
+ sendDTMF(tone: string): Promise<{ ok: true }>
128
+ transfer(to: string): Promise<{ ok: true }>
129
+ getStatus(): Promise<PhoneStatus>
130
+ setAuth(token: string): Promise<{ ok: true }>
131
+ logout(): Promise<{ ok: true }>
132
+
133
+ on<K extends keyof BravophoneEvents>(
134
+ event: K,
135
+ handler: (payload: BravophoneEvents[K]) => void,
136
+ ): () => void
137
+ off<K extends keyof BravophoneEvents>(
138
+ event: K,
139
+ handler: (payload: BravophoneEvents[K]) => void,
140
+ ): void
141
+
142
+ destroy(): void
143
+ readonly version: string
144
+ }
145
+
146
+ declare const Bravophone: BravophoneAPI
147
+ export default Bravophone
148
+
149
+ declare global {
150
+ interface Window {
151
+ Bravophone: BravophoneAPI
152
+ }
153
+ }