@callembed/elements 0.0.0-stage → 0.1.2

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/dist/index.js ADDED
@@ -0,0 +1,527 @@
1
+ import { CallEmbed, CallEmbedError } from '@callembed/sdk';
2
+ export { DEFAULT_HOSTED_EMBED_ORIGIN, DEFAULT_HOSTED_EMBED_URL } from '@callembed/sdk';
3
+ import { resolveCopy, friendlyErrorMessage } from '@callembed/sdk/ui';
4
+ export { enCopy, friendlyErrorMessage, resolveCopy, zhTwCopy } from '@callembed/sdk/ui';
5
+
6
+ // src/callembed-button.ts
7
+ function statusLabel(copy, uiState, sdkStatus) {
8
+ switch (uiState) {
9
+ case "idle":
10
+ return copy.ready;
11
+ case "ringing":
12
+ return sdkStatus === "connecting" ? copy.connecting : copy.ringing;
13
+ case "connected":
14
+ return copy.answered;
15
+ case "ended":
16
+ return copy.ended;
17
+ case "error":
18
+ return copy.error;
19
+ default:
20
+ return copy.ready;
21
+ }
22
+ }
23
+ function runtimeForCopy(locale, copyOverrides = {}) {
24
+ return {
25
+ publishableKey: "",
26
+ apiBaseUrl: "",
27
+ locale,
28
+ copyOverrides,
29
+ branding: true,
30
+ debug: false
31
+ };
32
+ }
33
+
34
+ // src/styles.ts
35
+ var ELEMENT_STYLES = `
36
+ :where(.ce-el-button, .ce-el-status, .ce-el-mute) {
37
+ font-family: var(
38
+ --ce-font,
39
+ "Noto Sans TC",
40
+ "IBM Plex Sans",
41
+ "Segoe UI",
42
+ sans-serif
43
+ );
44
+ color: var(--ce-fg, #e8eef4);
45
+ }
46
+
47
+ .ce-el-root {
48
+ display: inline-flex;
49
+ flex-direction: column;
50
+ align-items: flex-start;
51
+ gap: 0.5rem;
52
+ }
53
+
54
+ .ce-el-button {
55
+ appearance: none;
56
+ display: inline-block;
57
+ border: 1px solid var(--ce-border, #2a3a46);
58
+ border-radius: var(--ce-radius, 0);
59
+ background: color-mix(in srgb, var(--ce-accent, #5eb0d8) 25%, transparent);
60
+ color: var(--ce-fg, #e8eef4);
61
+ padding: 0.7rem 1.1rem;
62
+ font-size: inherit;
63
+ cursor: pointer;
64
+ }
65
+
66
+ .ce-el-button:hover {
67
+ border-color: var(--ce-accent, #5eb0d8);
68
+ }
69
+
70
+ .ce-el-button:disabled {
71
+ opacity: 0.5;
72
+ cursor: not-allowed;
73
+ }
74
+
75
+ .ce-el-button[data-state="ringing"],
76
+ .ce-el-button[data-state="connected"] {
77
+ border-color: color-mix(
78
+ in srgb,
79
+ var(--ce-danger, #c44b4b) 55%,
80
+ var(--ce-border, #2a3a46)
81
+ );
82
+ color: var(--ce-error, #f0c4c4);
83
+ background: transparent;
84
+ }
85
+
86
+ .ce-el-status {
87
+ margin: 0;
88
+ color: var(--ce-muted, #9aadb8);
89
+ font-size: 0.9rem;
90
+ line-height: 1.4;
91
+ min-height: 1.25em;
92
+ }
93
+
94
+ .ce-el-status[data-tone="error"] {
95
+ color: var(--ce-error, #f0c4c4);
96
+ }
97
+
98
+ .ce-el-status[data-tone="ok"] {
99
+ color: var(--ce-ok, #9fd6c0);
100
+ }
101
+
102
+ .ce-el-mute {
103
+ appearance: none;
104
+ border: 1px solid var(--ce-border, #2a3a46);
105
+ border-radius: var(--ce-radius, 0);
106
+ background: transparent;
107
+ color: inherit;
108
+ padding: 0.45rem 0.75rem;
109
+ font: inherit;
110
+ cursor: pointer;
111
+ }
112
+
113
+ .ce-el-mute:hover {
114
+ border-color: var(--ce-accent, #5eb0d8);
115
+ }
116
+
117
+ .ce-el-mute:disabled {
118
+ opacity: 0.5;
119
+ cursor: not-allowed;
120
+ }
121
+ `;
122
+
123
+ // src/types.ts
124
+ function toUiState(status) {
125
+ switch (status) {
126
+ case "connecting":
127
+ case "ringing":
128
+ return "ringing";
129
+ case "answered":
130
+ return "connected";
131
+ case "ended":
132
+ return "ended";
133
+ case "error":
134
+ return "error";
135
+ case "idle":
136
+ default:
137
+ return "idle";
138
+ }
139
+ }
140
+
141
+ // src/callembed-button.ts
142
+ var TAG = "callembed-button";
143
+ function toErrorState(err) {
144
+ if (err instanceof CallEmbedError) {
145
+ return { code: err.code, message: err.message };
146
+ }
147
+ return {
148
+ code: "call_failed",
149
+ message: err instanceof Error ? err.message : String(err)
150
+ };
151
+ }
152
+ function isActiveStatus(status) {
153
+ return status === "connecting" || status === "ringing" || status === "answered";
154
+ }
155
+ function parseLocale(raw) {
156
+ if (raw === "en" || raw === "zh-TW") return raw;
157
+ return void 0;
158
+ }
159
+ var CallEmbedButtonElement = class extends HTMLElement {
160
+ static get observedAttributes() {
161
+ return [
162
+ "publishable-key",
163
+ "api-base-url",
164
+ "locale",
165
+ "queue",
166
+ "label",
167
+ "show-status",
168
+ "show-mute",
169
+ "branding",
170
+ "debug"
171
+ ];
172
+ }
173
+ #status = "idle";
174
+ #callId = null;
175
+ #muted = false;
176
+ #error = null;
177
+ #locale = "en";
178
+ #busy = false;
179
+ #call = null;
180
+ #unbind = null;
181
+ #root = null;
182
+ #styleEl = null;
183
+ #initialized = false;
184
+ #attrInitKey = null;
185
+ get uiState() {
186
+ return toUiState(this.#status);
187
+ }
188
+ get callStatus() {
189
+ return this.#status;
190
+ }
191
+ get callId() {
192
+ return this.#callId;
193
+ }
194
+ get muted() {
195
+ return this.#muted;
196
+ }
197
+ get error() {
198
+ return this.#error;
199
+ }
200
+ connectedCallback() {
201
+ this.#ensureDom();
202
+ this.#syncFromAttributes(
203
+ /* hangUpOnEndpointChange */
204
+ false
205
+ );
206
+ this.#render();
207
+ }
208
+ disconnectedCallback() {
209
+ this.#hangUpActive();
210
+ }
211
+ attributeChangedCallback(name, oldValue, newValue) {
212
+ if (oldValue === newValue) return;
213
+ if (!this.isConnected) return;
214
+ const endpointChanged = name === "publishable-key" || name === "api-base-url";
215
+ this.#syncFromAttributes(endpointChanged);
216
+ this.#render();
217
+ }
218
+ /** Start a queue call (customer-service ring → agent answers). */
219
+ async start() {
220
+ return this.#start();
221
+ }
222
+ /** Hang up the active call (idempotent). */
223
+ async end() {
224
+ return this.#end();
225
+ }
226
+ async setMuted(muted) {
227
+ const call = this.#call ?? CallEmbed.getActiveCall();
228
+ if (!call) {
229
+ this.#muted = muted;
230
+ this.#emitState();
231
+ this.#render();
232
+ return;
233
+ }
234
+ await call.setMuted(muted);
235
+ }
236
+ #ensureDom() {
237
+ if (this.#root) return;
238
+ this.#styleEl = document.createElement("style");
239
+ this.#styleEl.textContent = ELEMENT_STYLES;
240
+ this.appendChild(this.#styleEl);
241
+ this.#root = document.createElement("div");
242
+ this.#root.className = "ce-el-root";
243
+ this.#root.setAttribute("data-ce-el", "");
244
+ this.appendChild(this.#root);
245
+ }
246
+ #syncFromAttributes(hangUpOnEndpointChange) {
247
+ const publishableKey = this.getAttribute("publishable-key") ?? "";
248
+ const apiBaseUrl = this.getAttribute("api-base-url");
249
+ const locale = parseLocale(this.getAttribute("locale"));
250
+ const brandingAttr = this.getAttribute("branding");
251
+ const branding = brandingAttr !== "false";
252
+ const debug = this.getAttribute("debug") === "true";
253
+ const initKey = JSON.stringify({
254
+ publishableKey,
255
+ apiBaseUrl: apiBaseUrl ?? "",
256
+ locale: locale ?? null,
257
+ branding,
258
+ debug
259
+ });
260
+ if (hangUpOnEndpointChange && this.#attrInitKey !== null) {
261
+ const prev = JSON.parse(this.#attrInitKey);
262
+ if (prev.publishableKey !== publishableKey || prev.apiBaseUrl !== (apiBaseUrl ?? "")) {
263
+ this.#hangUpActive();
264
+ }
265
+ }
266
+ if (initKey === this.#attrInitKey && this.#initialized) {
267
+ return;
268
+ }
269
+ this.#attrInitKey = initKey;
270
+ if (!publishableKey || apiBaseUrl === null) {
271
+ return;
272
+ }
273
+ try {
274
+ CallEmbed.init({
275
+ publishableKey,
276
+ apiBaseUrl,
277
+ locale,
278
+ branding,
279
+ debug
280
+ });
281
+ const runtime = CallEmbed._getRuntime();
282
+ if (runtime) this.#locale = runtime.locale;
283
+ this.#initialized = true;
284
+ } catch (err) {
285
+ const state = toErrorState(err);
286
+ this.#error = state;
287
+ this.#status = "error";
288
+ this.#emitError(state);
289
+ this.#emitState();
290
+ }
291
+ }
292
+ #hangUpActive() {
293
+ this.#unbind?.();
294
+ this.#unbind = null;
295
+ const call = this.#call ?? CallEmbed.getActiveCall();
296
+ this.#call = null;
297
+ if (call && isActiveStatus(call.status)) {
298
+ void call.hangUp();
299
+ }
300
+ }
301
+ async #start() {
302
+ try {
303
+ const queue = this.getAttribute("queue") ?? "";
304
+ if (!queue.trim()) {
305
+ throw new CallEmbedError(
306
+ "invalid_queue",
307
+ "queue is required (set the queue attribute)."
308
+ );
309
+ }
310
+ this.#error = null;
311
+ this.#muted = false;
312
+ const call = await CallEmbed.startCall({
313
+ queue,
314
+ hidePanel: true,
315
+ on: {
316
+ statusChange: ({ status }) => {
317
+ this.#status = status;
318
+ this.#emitState();
319
+ this.#render();
320
+ },
321
+ muteChange: ({ muted }) => {
322
+ this.#muted = muted;
323
+ this.#emitState();
324
+ this.#render();
325
+ },
326
+ error: ({ code, message, callId }) => {
327
+ this.#error = { code, message };
328
+ this.#status = "error";
329
+ if (callId && callId !== "pending") this.#callId = callId;
330
+ this.#emitError(this.#error);
331
+ this.#emitState();
332
+ this.#render();
333
+ },
334
+ ended: ({ callId }) => {
335
+ this.#status = "ended";
336
+ if (callId && callId !== "pending") this.#callId = callId;
337
+ this.#emitState();
338
+ this.#render();
339
+ },
340
+ ringing: ({ callId }) => {
341
+ this.#callId = callId;
342
+ this.#emitState();
343
+ },
344
+ answered: ({ callId }) => {
345
+ this.#callId = callId;
346
+ this.#emitState();
347
+ }
348
+ }
349
+ });
350
+ this.#unbind?.();
351
+ this.#call = call;
352
+ this.#status = call.status;
353
+ if (call.callId !== "pending") this.#callId = call.callId;
354
+ this.#unbind = this.#bindCall(call);
355
+ this.#emitState();
356
+ this.#render();
357
+ return call;
358
+ } catch (err) {
359
+ const state = toErrorState(err);
360
+ this.#error = state;
361
+ this.#status = "error";
362
+ this.#emitError(state);
363
+ this.#emitState();
364
+ this.#render();
365
+ if (err instanceof CallEmbedError) throw err;
366
+ throw new CallEmbedError("call_failed", state.message);
367
+ }
368
+ }
369
+ async #end() {
370
+ const call = this.#call ?? CallEmbed.getActiveCall();
371
+ if (!call) {
372
+ this.#status = "idle";
373
+ this.#emitState();
374
+ this.#render();
375
+ return;
376
+ }
377
+ await call.hangUp();
378
+ }
379
+ #bindCall(call) {
380
+ const offs = [
381
+ call.on("statusChange", ({ status }) => {
382
+ this.#status = status;
383
+ if (call.callId && call.callId !== "pending") {
384
+ this.#callId = call.callId;
385
+ }
386
+ this.#emitState();
387
+ this.#render();
388
+ }),
389
+ call.on("muteChange", ({ muted }) => {
390
+ this.#muted = muted;
391
+ this.#emitState();
392
+ this.#render();
393
+ }),
394
+ call.on("error", ({ code, message, callId }) => {
395
+ this.#error = { code, message };
396
+ this.#status = "error";
397
+ if (callId && callId !== "pending") this.#callId = callId;
398
+ this.#emitError(this.#error);
399
+ this.#emitState();
400
+ this.#render();
401
+ }),
402
+ call.on("ended", ({ callId }) => {
403
+ this.#status = "ended";
404
+ if (callId && callId !== "pending") this.#callId = callId;
405
+ this.#emitState();
406
+ this.#render();
407
+ })
408
+ ];
409
+ return () => {
410
+ offs.forEach((off) => off());
411
+ };
412
+ }
413
+ #emitState() {
414
+ this.dispatchEvent(
415
+ new CustomEvent("callembed:state", {
416
+ bubbles: true,
417
+ composed: true,
418
+ detail: {
419
+ uiState: this.uiState,
420
+ status: this.#status,
421
+ callId: this.#callId,
422
+ muted: this.#muted,
423
+ error: this.#error
424
+ }
425
+ })
426
+ );
427
+ }
428
+ #emitError(error) {
429
+ this.dispatchEvent(
430
+ new CustomEvent("callembed:error", {
431
+ bubbles: true,
432
+ composed: true,
433
+ detail: { code: error.code, message: error.message }
434
+ })
435
+ );
436
+ }
437
+ #showStatus() {
438
+ const v = this.getAttribute("show-status");
439
+ return v !== "false";
440
+ }
441
+ #showMute() {
442
+ const v = this.getAttribute("show-mute");
443
+ return v !== "false";
444
+ }
445
+ #render() {
446
+ if (!this.#root) return;
447
+ const copy = resolveCopy(runtimeForCopy(this.#locale));
448
+ const ui = this.uiState;
449
+ const inCall = ui === "ringing" || ui === "connected";
450
+ const labelAttr = this.getAttribute("label");
451
+ const buttonLabel = inCall ? copy.hangup : labelAttr ?? copy.launchDefault;
452
+ const statusText = ui === "error" && this.#error ? friendlyErrorMessage(copy, this.#error.code, this.#error.message) : statusLabel(copy, ui, this.#status);
453
+ const tone = ui === "error" ? "error" : ui === "connected" ? "ok" : void 0;
454
+ this.#root.replaceChildren();
455
+ const btn = document.createElement("button");
456
+ btn.type = "button";
457
+ btn.className = "ce-el-button";
458
+ btn.dataset.state = ui;
459
+ btn.disabled = this.#busy;
460
+ if (this.#busy) btn.setAttribute("aria-busy", "true");
461
+ btn.textContent = buttonLabel;
462
+ btn.addEventListener("click", () => {
463
+ void this.#onMainClick();
464
+ });
465
+ this.#root.appendChild(btn);
466
+ if (this.#showMute() && inCall) {
467
+ const muteBtn = document.createElement("button");
468
+ muteBtn.type = "button";
469
+ muteBtn.className = "ce-el-mute";
470
+ muteBtn.disabled = this.#busy;
471
+ muteBtn.textContent = this.#muted ? copy.unmute : copy.mute;
472
+ muteBtn.addEventListener("click", () => {
473
+ void this.#onMuteClick();
474
+ });
475
+ this.#root.appendChild(muteBtn);
476
+ }
477
+ if (this.#showStatus()) {
478
+ const statusEl = document.createElement("p");
479
+ statusEl.className = "ce-el-status";
480
+ statusEl.dataset.state = ui;
481
+ if (tone) statusEl.dataset.tone = tone;
482
+ statusEl.textContent = statusText;
483
+ this.#root.appendChild(statusEl);
484
+ }
485
+ }
486
+ async #onMainClick() {
487
+ if (this.#busy) return;
488
+ this.#busy = true;
489
+ this.#render();
490
+ try {
491
+ const ui = this.uiState;
492
+ if (ui === "ringing" || ui === "connected") {
493
+ await this.#end();
494
+ } else {
495
+ await this.#start();
496
+ }
497
+ } catch {
498
+ } finally {
499
+ this.#busy = false;
500
+ this.#render();
501
+ }
502
+ }
503
+ async #onMuteClick() {
504
+ if (this.#busy) return;
505
+ this.#busy = true;
506
+ this.#render();
507
+ try {
508
+ await this.setMuted(!this.#muted);
509
+ } catch {
510
+ } finally {
511
+ this.#busy = false;
512
+ this.#render();
513
+ }
514
+ }
515
+ };
516
+ function defineCallEmbedButton(tagName = TAG) {
517
+ if (typeof customElements === "undefined") {
518
+ return CallEmbedButtonElement;
519
+ }
520
+ if (!customElements.get(tagName)) {
521
+ customElements.define(tagName, CallEmbedButtonElement);
522
+ }
523
+ return CallEmbedButtonElement;
524
+ }
525
+ defineCallEmbedButton();
526
+
527
+ export { TAG as CALLEMBED_BUTTON_TAG, CallEmbedButtonElement, defineCallEmbedButton, statusLabel, toUiState };
package/package.json CHANGED
@@ -1,6 +1,83 @@
1
1
  {
2
2
  "name": "@callembed/elements",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.2",
4
+ "description": "Framework-agnostic CallEmbed Web Component (thin wrapper over @callembed/sdk)",
5
+ "license": "MIT",
6
+ "author": "Sealby",
7
+ "type": "module",
8
+ "main": "./dist/index.cjs",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "unpkg": "./dist/callembed-elements.iife.js",
12
+ "jsdelivr": "./dist/callembed-elements.iife.js",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js",
17
+ "require": "./dist/index.cjs",
18
+ "default": "./dist/index.js"
19
+ },
20
+ "./package.json": "./package.json"
21
+ },
22
+ "sideEffects": [
23
+ "./dist/index.js",
24
+ "./dist/index.cjs",
25
+ "./dist/callembed-elements.iife.js"
26
+ ],
27
+ "files": [
28
+ "dist",
29
+ "README.md",
30
+ "LICENSE"
31
+ ],
32
+ "scripts": {
33
+ "build": "tsup && node ./scripts/build-iife.mjs",
34
+ "prepack": "npm run build",
35
+ "typecheck": "tsc -p tsconfig.json --noEmit",
36
+ "dev": "tsup --watch",
37
+ "test": "vitest run",
38
+ "test:watch": "vitest",
39
+ "pack:verify": "node ./scripts/pack-verify.mjs"
40
+ },
41
+ "homepage": "https://callembed.sealby.me",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/TonyFeng1203/CallEmbed.git",
45
+ "directory": "packages/elements"
46
+ },
47
+ "bugs": {
48
+ "url": "https://github.com/TonyFeng1203/CallEmbed/issues"
49
+ },
50
+ "keywords": [
51
+ "callembed",
52
+ "web-components",
53
+ "custom-elements",
54
+ "voice",
55
+ "webrtc",
56
+ "livekit",
57
+ "customer-support"
58
+ ],
59
+ "engines": {
60
+ "node": ">=20"
61
+ },
62
+ "publishConfig": {
63
+ "access": "public"
64
+ },
65
+ "peerDependencies": {
66
+ "@callembed/sdk": "^0.1.2",
67
+ "livekit-client": "^2.13.0"
68
+ },
69
+ "peerDependenciesMeta": {
70
+ "livekit-client": {
71
+ "optional": true
72
+ }
73
+ },
74
+ "devDependencies": {
75
+ "@callembed/sdk": "^0.1.2",
76
+ "esbuild": "0.25.4",
77
+ "jsdom": "26.1.0",
78
+ "livekit-client": "^2.13.0",
79
+ "tsup": "^8.4.0",
80
+ "typescript": "^5.8.3",
81
+ "vitest": "3.2.7"
82
+ }
83
+ }