@callembed/vue 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,444 @@
1
+ import { CallEmbed, CallEmbedError } from '@callembed/sdk';
2
+ import { defineComponent, ref, computed, watch, reactive, provide, h, inject } from 'vue';
3
+ import { resolveCopy, friendlyErrorMessage } from '@callembed/sdk/ui';
4
+ export { enCopy, friendlyErrorMessage, resolveCopy, zhTwCopy } from '@callembed/sdk/ui';
5
+
6
+ // src/provider.ts
7
+
8
+ // src/types.ts
9
+ function toUiState(status) {
10
+ switch (status) {
11
+ case "connecting":
12
+ case "ringing":
13
+ return "ringing";
14
+ case "answered":
15
+ return "connected";
16
+ case "ended":
17
+ return "ended";
18
+ case "error":
19
+ return "error";
20
+ case "idle":
21
+ default:
22
+ return "idle";
23
+ }
24
+ }
25
+ var CALL_EMBED_KEY = /* @__PURE__ */ Symbol("callembed");
26
+
27
+ // src/provider.ts
28
+ function toErrorState(err) {
29
+ if (err instanceof CallEmbedError) {
30
+ return { code: err.code, message: err.message };
31
+ }
32
+ return {
33
+ code: "call_failed",
34
+ message: err instanceof Error ? err.message : String(err)
35
+ };
36
+ }
37
+ function isActiveStatus(status) {
38
+ return status === "connecting" || status === "ringing" || status === "answered";
39
+ }
40
+ function bindCall(call, handlers) {
41
+ const offs = [
42
+ call.on("statusChange", ({ status }) => {
43
+ handlers.setStatus(status);
44
+ if (call.callId && call.callId !== "pending") {
45
+ handlers.setCallId(call.callId);
46
+ }
47
+ }),
48
+ call.on("muteChange", ({ muted }) => {
49
+ handlers.setMuted(muted);
50
+ }),
51
+ call.on("error", ({ code, message, callId }) => {
52
+ handlers.setError({ code, message });
53
+ handlers.setStatus("error");
54
+ if (callId && callId !== "pending") handlers.setCallId(callId);
55
+ }),
56
+ call.on("ended", ({ callId }) => {
57
+ handlers.setStatus("ended");
58
+ if (callId && callId !== "pending") handlers.setCallId(callId);
59
+ })
60
+ ];
61
+ return () => {
62
+ offs.forEach((off) => off());
63
+ };
64
+ }
65
+ var CallEmbedProvider = defineComponent({
66
+ name: "CallEmbedProvider",
67
+ props: {
68
+ publishableKey: { type: String, required: true },
69
+ apiBaseUrl: { type: String, required: true },
70
+ locale: { type: String, default: void 0 },
71
+ copy: {
72
+ type: Object,
73
+ default: void 0
74
+ },
75
+ branding: { type: Boolean, default: void 0 },
76
+ debug: { type: Boolean, default: void 0 },
77
+ queue: { type: String, default: void 0 }
78
+ },
79
+ slots: Object,
80
+ setup(props, { slots }) {
81
+ const status = ref("idle");
82
+ const callId = ref(null);
83
+ const muted = ref(false);
84
+ const error = ref(null);
85
+ const resolvedLocale = ref(props.locale ?? "en");
86
+ const callRef = ref(null);
87
+ let unbind = null;
88
+ const copyKey = computed(() => JSON.stringify(props.copy ?? {}));
89
+ const brandingValue = computed(() => props.branding !== false);
90
+ function hangUpActive() {
91
+ unbind?.();
92
+ unbind = null;
93
+ const call = callRef.value ?? CallEmbed.getActiveCall();
94
+ callRef.value = null;
95
+ if (call && isActiveStatus(call.status)) {
96
+ void call.hangUp();
97
+ }
98
+ }
99
+ function runInit() {
100
+ if (typeof window === "undefined") return;
101
+ try {
102
+ CallEmbed.init({
103
+ publishableKey: props.publishableKey,
104
+ apiBaseUrl: props.apiBaseUrl,
105
+ locale: props.locale,
106
+ copy: props.copy,
107
+ branding: props.branding,
108
+ debug: props.debug
109
+ });
110
+ const runtime = CallEmbed._getRuntime();
111
+ if (runtime) resolvedLocale.value = runtime.locale;
112
+ } catch (err) {
113
+ const state = toErrorState(err);
114
+ error.value = state;
115
+ status.value = "error";
116
+ }
117
+ }
118
+ watch(
119
+ () => [
120
+ props.publishableKey,
121
+ props.apiBaseUrl,
122
+ props.locale,
123
+ copyKey.value,
124
+ brandingValue.value,
125
+ props.debug
126
+ ],
127
+ () => {
128
+ runInit();
129
+ },
130
+ { immediate: true }
131
+ );
132
+ watch(
133
+ () => [props.publishableKey, props.apiBaseUrl],
134
+ (_next, _prev, onCleanup) => {
135
+ onCleanup(() => {
136
+ hangUpActive();
137
+ });
138
+ },
139
+ { immediate: true }
140
+ );
141
+ async function start(options = {}) {
142
+ try {
143
+ const q = options.queue ?? props.queue;
144
+ if (!q?.trim()) {
145
+ throw new CallEmbedError(
146
+ "invalid_queue",
147
+ "queue is required (pass queue on the provider, button, or start())."
148
+ );
149
+ }
150
+ error.value = null;
151
+ muted.value = false;
152
+ const userOn = options.on;
153
+ const call = await CallEmbed.startCall({
154
+ ...options,
155
+ queue: q,
156
+ hidePanel: true,
157
+ on: {
158
+ ...userOn,
159
+ statusChange: (payload) => {
160
+ status.value = payload.status;
161
+ userOn?.statusChange?.(payload);
162
+ },
163
+ muteChange: (payload) => {
164
+ muted.value = payload.muted;
165
+ userOn?.muteChange?.(payload);
166
+ },
167
+ error: (payload) => {
168
+ error.value = { code: payload.code, message: payload.message };
169
+ status.value = "error";
170
+ if (payload.callId && payload.callId !== "pending") {
171
+ callId.value = payload.callId;
172
+ }
173
+ userOn?.error?.(payload);
174
+ },
175
+ ended: (payload) => {
176
+ status.value = "ended";
177
+ if (payload.callId && payload.callId !== "pending") {
178
+ callId.value = payload.callId;
179
+ }
180
+ userOn?.ended?.(payload);
181
+ },
182
+ ringing: (payload) => {
183
+ callId.value = payload.callId;
184
+ userOn?.ringing?.(payload);
185
+ },
186
+ answered: (payload) => {
187
+ callId.value = payload.callId;
188
+ userOn?.answered?.(payload);
189
+ }
190
+ }
191
+ });
192
+ unbind?.();
193
+ callRef.value = call;
194
+ status.value = call.status;
195
+ if (call.callId !== "pending") callId.value = call.callId;
196
+ unbind = bindCall(call, {
197
+ setStatus: (s) => {
198
+ status.value = s;
199
+ },
200
+ setCallId: (id) => {
201
+ callId.value = id;
202
+ },
203
+ setMuted: (m) => {
204
+ muted.value = m;
205
+ },
206
+ setError: (e) => {
207
+ error.value = e;
208
+ }
209
+ });
210
+ return call;
211
+ } catch (err) {
212
+ const state = toErrorState(err);
213
+ error.value = state;
214
+ status.value = "error";
215
+ if (err instanceof CallEmbedError) throw err;
216
+ throw new CallEmbedError("call_failed", state.message);
217
+ }
218
+ }
219
+ async function end() {
220
+ const call = callRef.value ?? CallEmbed.getActiveCall();
221
+ if (!call) {
222
+ status.value = "idle";
223
+ return;
224
+ }
225
+ await call.hangUp();
226
+ }
227
+ async function setMuted(next) {
228
+ const call = callRef.value ?? CallEmbed.getActiveCall();
229
+ if (!call) {
230
+ muted.value = next;
231
+ return;
232
+ }
233
+ await call.setMuted(next);
234
+ }
235
+ const api = reactive({
236
+ get status() {
237
+ return status.value;
238
+ },
239
+ get uiState() {
240
+ return toUiState(status.value);
241
+ },
242
+ get callId() {
243
+ return callId.value;
244
+ },
245
+ get muted() {
246
+ return muted.value;
247
+ },
248
+ get error() {
249
+ return error.value;
250
+ },
251
+ get locale() {
252
+ return resolvedLocale.value;
253
+ },
254
+ start,
255
+ end,
256
+ setMuted,
257
+ get defaultQueue() {
258
+ return props.queue;
259
+ }
260
+ });
261
+ provide(CALL_EMBED_KEY, api);
262
+ return () => slots.default?.();
263
+ }
264
+ });
265
+ function useCallEmbed() {
266
+ const ctx = inject(CALL_EMBED_KEY, null);
267
+ if (!ctx) {
268
+ throw new Error("useCallEmbed() must be used within <CallEmbedProvider>.");
269
+ }
270
+ return ctx;
271
+ }
272
+ function statusLabel(copy, uiState, sdkStatus) {
273
+ switch (uiState) {
274
+ case "idle":
275
+ return copy.ready;
276
+ case "ringing":
277
+ return sdkStatus === "connecting" ? copy.connecting : copy.ringing;
278
+ case "connected":
279
+ return copy.answered;
280
+ case "ended":
281
+ return copy.ended;
282
+ case "error":
283
+ return copy.error;
284
+ default:
285
+ return copy.ready;
286
+ }
287
+ }
288
+ function runtimeForCopy(locale, copyOverrides = {}) {
289
+ return {
290
+ publishableKey: "",
291
+ apiBaseUrl: "",
292
+ locale,
293
+ copyOverrides,
294
+ branding: true,
295
+ debug: false
296
+ };
297
+ }
298
+
299
+ // src/CallEmbedButton.ts
300
+ var CallEmbedButton = defineComponent({
301
+ name: "CallEmbedButton",
302
+ props: {
303
+ /** Queue to ring. Falls back to `CallEmbedProvider` `queue`. */
304
+ queue: { type: String, default: void 0 },
305
+ /** Idle-state label; defaults to locale `launchDefault`. */
306
+ label: { type: String, default: void 0 },
307
+ /** Optional metadata forwarded to `startCall`. */
308
+ metadata: {
309
+ type: Object,
310
+ default: void 0
311
+ },
312
+ /** Optional caller display name. */
313
+ displayName: { type: String, default: void 0 },
314
+ /** Show a status line under the button. Default: `true`. */
315
+ showStatus: { type: Boolean, default: true },
316
+ /** Show mute control while ringing/connected. Default: `true`. */
317
+ showMute: { type: Boolean, default: true },
318
+ disabled: { type: Boolean, default: false }
319
+ },
320
+ emits: {
321
+ error: (_error) => true
322
+ },
323
+ setup(props, { emit, attrs }) {
324
+ const embed = useCallEmbed();
325
+ const busy = ref(false);
326
+ const copy = computed(() => resolveCopy(runtimeForCopy(embed.locale)));
327
+ const inCall = computed(
328
+ () => embed.uiState === "ringing" || embed.uiState === "connected"
329
+ );
330
+ const buttonLabel = computed(
331
+ () => inCall.value ? copy.value.hangup : props.label ?? copy.value.launchDefault
332
+ );
333
+ const statusText = computed(() => {
334
+ if (embed.uiState === "error" && embed.error) {
335
+ return friendlyErrorMessage(
336
+ copy.value,
337
+ embed.error.code,
338
+ embed.error.message
339
+ );
340
+ }
341
+ return statusLabel(copy.value, embed.uiState, embed.status);
342
+ });
343
+ const tone = computed(
344
+ () => embed.uiState === "error" ? "error" : embed.uiState === "connected" ? "ok" : void 0
345
+ );
346
+ async function onClick() {
347
+ if (busy.value) return;
348
+ busy.value = true;
349
+ try {
350
+ if (inCall.value) {
351
+ await embed.end();
352
+ } else {
353
+ await embed.start({
354
+ queue: props.queue ?? embed.defaultQueue,
355
+ metadata: props.metadata,
356
+ displayName: props.displayName
357
+ });
358
+ }
359
+ } catch (err) {
360
+ const ce = err instanceof CallEmbedError ? err : new CallEmbedError(
361
+ "call_failed",
362
+ err instanceof Error ? err.message : String(err)
363
+ );
364
+ emit("error", ce);
365
+ } finally {
366
+ busy.value = false;
367
+ }
368
+ }
369
+ async function onMuteClick() {
370
+ if (busy.value) return;
371
+ busy.value = true;
372
+ try {
373
+ await embed.setMuted(!embed.muted);
374
+ } catch (err) {
375
+ const ce = err instanceof CallEmbedError ? err : new CallEmbedError(
376
+ "call_failed",
377
+ err instanceof Error ? err.message : String(err)
378
+ );
379
+ emit("error", ce);
380
+ } finally {
381
+ busy.value = false;
382
+ }
383
+ }
384
+ return () => {
385
+ const rootClass = ["ce-vue-root", attrs.class].filter(Boolean).join(" ");
386
+ const children = [
387
+ h(
388
+ "button",
389
+ {
390
+ type: "button",
391
+ class: "ce-vue-button",
392
+ "data-state": embed.uiState,
393
+ disabled: props.disabled || busy.value,
394
+ "aria-busy": busy.value || void 0,
395
+ onClick: () => {
396
+ void onClick();
397
+ }
398
+ },
399
+ buttonLabel.value
400
+ )
401
+ ];
402
+ if (props.showMute && inCall.value) {
403
+ children.push(
404
+ h(
405
+ "button",
406
+ {
407
+ type: "button",
408
+ class: "ce-vue-mute",
409
+ disabled: busy.value,
410
+ onClick: () => {
411
+ void onMuteClick();
412
+ }
413
+ },
414
+ embed.muted ? copy.value.unmute : copy.value.mute
415
+ )
416
+ );
417
+ }
418
+ if (props.showStatus) {
419
+ children.push(
420
+ h(
421
+ "p",
422
+ {
423
+ class: "ce-vue-status",
424
+ "data-tone": tone.value,
425
+ "data-state": embed.uiState
426
+ },
427
+ statusText.value
428
+ )
429
+ );
430
+ }
431
+ return h("div", { class: rootClass, "data-ce-vue": "" }, children);
432
+ };
433
+ }
434
+ });
435
+
436
+ // src/plugin.ts
437
+ var CallEmbedPlugin = {
438
+ install(app, options = {}) {
439
+ app.component(options.providerName ?? "CallEmbedProvider", CallEmbedProvider);
440
+ app.component(options.buttonName ?? "CallEmbedButton", CallEmbedButton);
441
+ }
442
+ };
443
+
444
+ export { CALL_EMBED_KEY, CallEmbedButton, CallEmbedPlugin, CallEmbedProvider, statusLabel, toUiState, useCallEmbed };
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Default styles for `@callembed/vue`.
3
+ * Theme via `--ce-*` (same public tokens as `@callembed/sdk/ui`).
4
+ */
5
+ :where(.ce-vue-button, .ce-vue-status, .ce-vue-mute) {
6
+ font-family: var(
7
+ --ce-font,
8
+ "Noto Sans TC",
9
+ "IBM Plex Sans",
10
+ "Segoe UI",
11
+ sans-serif
12
+ );
13
+ color: var(--ce-fg, #e8eef4);
14
+ }
15
+
16
+ .ce-vue-root {
17
+ display: inline-flex;
18
+ flex-direction: column;
19
+ align-items: flex-start;
20
+ gap: 0.5rem;
21
+ }
22
+
23
+ .ce-vue-button {
24
+ appearance: none;
25
+ display: inline-block;
26
+ border: 1px solid var(--ce-border, #2a3a46);
27
+ border-radius: var(--ce-radius, 0);
28
+ background: color-mix(in srgb, var(--ce-accent, #5eb0d8) 25%, transparent);
29
+ color: var(--ce-fg, #e8eef4);
30
+ padding: 0.7rem 1.1rem;
31
+ font-size: inherit;
32
+ cursor: pointer;
33
+ }
34
+
35
+ .ce-vue-button:hover {
36
+ border-color: var(--ce-accent, #5eb0d8);
37
+ }
38
+
39
+ .ce-vue-button:disabled {
40
+ opacity: 0.5;
41
+ cursor: not-allowed;
42
+ }
43
+
44
+ .ce-vue-button[data-state="ringing"],
45
+ .ce-vue-button[data-state="connected"] {
46
+ border-color: color-mix(
47
+ in srgb,
48
+ var(--ce-danger, #c44b4b) 55%,
49
+ var(--ce-border, #2a3a46)
50
+ );
51
+ color: var(--ce-error, #f0c4c4);
52
+ background: transparent;
53
+ }
54
+
55
+ .ce-vue-status {
56
+ margin: 0;
57
+ color: var(--ce-muted, #9aadb8);
58
+ font-size: 0.9rem;
59
+ line-height: 1.4;
60
+ min-height: 1.25em;
61
+ }
62
+
63
+ .ce-vue-status[data-tone="error"] {
64
+ color: var(--ce-error, #f0c4c4);
65
+ }
66
+
67
+ .ce-vue-status[data-tone="ok"] {
68
+ color: var(--ce-ok, #9fd6c0);
69
+ }
70
+
71
+ .ce-vue-mute {
72
+ appearance: none;
73
+ border: 1px solid var(--ce-border, #2a3a46);
74
+ border-radius: var(--ce-radius, 0);
75
+ background: transparent;
76
+ color: inherit;
77
+ padding: 0.45rem 0.75rem;
78
+ font: inherit;
79
+ cursor: pointer;
80
+ }
81
+
82
+ .ce-vue-mute:hover {
83
+ border-color: var(--ce-accent, #5eb0d8);
84
+ }
85
+
86
+ .ce-vue-mute:disabled {
87
+ opacity: 0.5;
88
+ cursor: not-allowed;
89
+ }
package/package.json CHANGED
@@ -1,6 +1,82 @@
1
1
  {
2
2
  "name": "@callembed/vue",
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": "Vue 3 components and composables for CallEmbed (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
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js",
15
+ "require": "./dist/index.cjs",
16
+ "default": "./dist/index.js"
17
+ },
18
+ "./styles.css": "./dist/styles.css",
19
+ "./package.json": "./package.json"
20
+ },
21
+ "sideEffects": [
22
+ "./dist/styles.css"
23
+ ],
24
+ "files": [
25
+ "dist",
26
+ "README.md",
27
+ "LICENSE"
28
+ ],
29
+ "scripts": {
30
+ "build": "tsup && node ./scripts/copy-css.mjs",
31
+ "prepack": "npm run build",
32
+ "typecheck": "tsc -p tsconfig.json --noEmit",
33
+ "dev": "tsup --watch",
34
+ "test": "vitest run",
35
+ "test:watch": "vitest",
36
+ "pack:verify": "node ./scripts/pack-verify.mjs"
37
+ },
38
+ "homepage": "https://callembed.sealby.me",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/TonyFeng1203/CallEmbed.git",
42
+ "directory": "packages/vue"
43
+ },
44
+ "bugs": {
45
+ "url": "https://github.com/TonyFeng1203/CallEmbed/issues"
46
+ },
47
+ "keywords": [
48
+ "callembed",
49
+ "vue",
50
+ "nuxt",
51
+ "voice",
52
+ "webrtc",
53
+ "livekit",
54
+ "customer-support"
55
+ ],
56
+ "engines": {
57
+ "node": ">=20"
58
+ },
59
+ "publishConfig": {
60
+ "access": "public"
61
+ },
62
+ "peerDependencies": {
63
+ "@callembed/sdk": "^0.1.2",
64
+ "livekit-client": "^2.13.0",
65
+ "vue": "^3.4.0"
66
+ },
67
+ "peerDependenciesMeta": {
68
+ "livekit-client": {
69
+ "optional": true
70
+ }
71
+ },
72
+ "devDependencies": {
73
+ "@callembed/sdk": "^0.1.2",
74
+ "@vue/test-utils": "^2.4.6",
75
+ "jsdom": "26.1.0",
76
+ "livekit-client": "^2.13.0",
77
+ "tsup": "^8.4.0",
78
+ "typescript": "^5.8.3",
79
+ "vitest": "3.2.7",
80
+ "vue": "^3.5.13"
81
+ }
82
+ }