@intray/support 0.7.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/LICENSE +21 -0
- package/README.md +260 -0
- package/dist/chunk-3IC2MERJ.js +1 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +230 -0
- package/dist/server.d.ts +24 -0
- package/dist/server.js +1 -0
- package/dist/types-CLIkWasY.d.ts +105 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lowside Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# @intray/support
|
|
2
|
+
|
|
3
|
+
A support chat widget for any web app, plus a server helper that talks to the Intray support endpoint. The widget has no framework dependency and no runtime dependencies.
|
|
4
|
+
|
|
5
|
+
The package has two entries:
|
|
6
|
+
|
|
7
|
+
| Entry | Runs in | Exports |
|
|
8
|
+
| ------------------------ | ------------ | -------------------------------------------- |
|
|
9
|
+
| `@intray/support` | browser | `createSupportWidget`, types, `SupportError` |
|
|
10
|
+
| `@intray/support/server` | your backend | `createSupportClient`, types, `SupportError` |
|
|
11
|
+
|
|
12
|
+
The browser entry never imports the server entry. The workspace key stays on your backend.
|
|
13
|
+
|
|
14
|
+
## How the parts connect
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
widget (browser) -> your transport -> your backend route -> createSupportClient -> Intray /v1/support/chat
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The browser never sends identity. Your backend route reads the signed-in user from its own session, builds a `SupportIdentity`, and calls Intray with the workspace key. A customer can only read and write conversations that belong to the identity your backend verified.
|
|
21
|
+
|
|
22
|
+
## Browser
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { createSupportWidget, type SupportTransport } from "@intray/support";
|
|
26
|
+
|
|
27
|
+
const call = async (operation: string, args: object = {}) => {
|
|
28
|
+
const response = await fetch("/api/support", {
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers: { "Content-Type": "application/json" },
|
|
31
|
+
body: JSON.stringify({ operation, ...args }),
|
|
32
|
+
});
|
|
33
|
+
if (!response.ok)
|
|
34
|
+
throw new Error(`Support request failed: ${response.status}`);
|
|
35
|
+
return (await response.json()).data;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const transport: SupportTransport = {
|
|
39
|
+
listConversations: (args) => call("list", args),
|
|
40
|
+
createConversation: (input) => call("create", input),
|
|
41
|
+
listMessages: (input) => call("messages", input),
|
|
42
|
+
sendMessage: (input) => call("send", input),
|
|
43
|
+
markRead: (input) => call("read", input),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const widget = createSupportWidget({
|
|
47
|
+
transport,
|
|
48
|
+
title: "Support",
|
|
49
|
+
notifications: true, // default
|
|
50
|
+
onError: (error) => console.warn(error),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
widget.open(); // optional, the launcher opens it too
|
|
54
|
+
widget.destroy(); // on sign out or unmount
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`createSupportWidget` mounts one host element with a "Contact support" launcher. It returns `open()`, `close()`, `destroy()` and `controlsContainer`. `open("messages")` shows the conversation list. Call `destroy()` when the signed-in user changes, then create a new widget.
|
|
58
|
+
|
|
59
|
+
Widget behavior:
|
|
60
|
+
|
|
61
|
+
- `open()` loads the latest page of conversations. Selecting a conversation loads its newest page of messages.
|
|
62
|
+
- "Older conversations" loads the next page with `nextCursor`, and "Latest conversations" returns to the first page. Every conversation stays reachable. The widget keeps one conversation page in memory.
|
|
63
|
+
- While the panel is open and the tab is visible, the widget polls the newest page every 5 seconds. A poll of the list refreshes the conversation page on screen, so an older page stays selected. It stops when the panel closes or the tab is hidden. Failed polls back off up to 60 seconds.
|
|
64
|
+
- Unread replies show in two places. A conversation row with `unread: true` gets a dot and the screen reader text "Unread." The closed launcher gets a dot, and its accessible name changes from "Contact support" to "Contact support - new messages". The launcher never shows message content.
|
|
65
|
+
- The launcher state is `hasUnread` from the backend. It covers every conversation of the customer, so an unread conversation outside the loaded page still shows.
|
|
66
|
+
- While the panel is closed and the tab is visible, the widget requests the latest conversation page every 15 seconds and reads only `hasUnread`. It makes one request on a visible mount. Failed checks back off to 60 seconds. It makes no request while the tab is hidden or after `destroy()`. `notifications: false` turns the launcher indicator off, and the closed widget then makes no request at all.
|
|
67
|
+
- The widget calls `markRead` only after it rendered messages in an open thread on a visible tab. `messageId` is the newest operator message on screen. A list fetch, a message fetch alone, a closed panel, a hidden tab and a load from a thread the customer left never count as read.
|
|
68
|
+
- One operator message is acknowledged once. A failed `markRead` goes to `onError`, shows no status text, and retries on a later poll with backoff up to 60 seconds. The widget uses the `hasUnread` of the result for the launcher, unless a newer reply rendered or the customer moved on while the call ran. A newer reply gets its own call.
|
|
69
|
+
- "Load older messages" pages back with `nextCursor`. The widget keeps at most 500 messages and 20 history pages of one conversation in memory.
|
|
70
|
+
- Enter sends. Shift+Enter adds a newline.
|
|
71
|
+
- A transport promise, `markRead` included, that does not settle within 15 seconds fails with a `SupportError` of code `timeout`. A result that arrives later is dropped. Sending and polling then continue as after any other failure.
|
|
72
|
+
- A failed send keeps the draft. A retry of the same text reuses the same `requestId`, so the server stores the message once. Edited text gets a new `requestId`.
|
|
73
|
+
- A closed conversation shows a notice. The customer can still send a message, and the backend reopens the conversation.
|
|
74
|
+
- Drafts stay in memory only. The widget writes nothing to `localStorage`, `sessionStorage` or cookies, so one account never sees another account's text on a shared browser.
|
|
75
|
+
- All customer and operator text is set with `textContent`. The widget never parses HTML.
|
|
76
|
+
- The widget validates every transport result against the contract and rejects a malformed one.
|
|
77
|
+
- The status line shows fixed copy. Raw errors go to `onError` only.
|
|
78
|
+
- After `destroy()`, poll and timeout timers stop, every transport call in flight is rejected inside the widget so its caller ends, nothing reaches `onError`, and the host element is removed. The widget cannot abort your transport call. Its late result is dropped.
|
|
79
|
+
|
|
80
|
+
### Session recording
|
|
81
|
+
|
|
82
|
+
The widget is excluded from session recordings. The host element has `data-private`, `data-live-block`, `rr-block` and `ph-no-capture`, which `@intray/live`, rrweb and PostHog block by default. The host has no size, and the content is in a closed shadow root that rrweb cannot serialize.
|
|
83
|
+
|
|
84
|
+
## Server
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { createSupportClient } from "@intray/support/server";
|
|
88
|
+
|
|
89
|
+
const support = createSupportClient({
|
|
90
|
+
url: "https://<your-intray-deployment>/v1/support/chat", // full endpoint URL
|
|
91
|
+
key: process.env.INTRAY_SUPPORT_KEY!,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// POST /api/support
|
|
95
|
+
export async function handler(request: Request) {
|
|
96
|
+
const session = await requireSession(request); // your own auth
|
|
97
|
+
const { operation, ...args } = await request.json();
|
|
98
|
+
|
|
99
|
+
const transport = support.forIdentity({
|
|
100
|
+
userId: session.user.id,
|
|
101
|
+
accountId: session.account.id,
|
|
102
|
+
displayName: session.user.name,
|
|
103
|
+
origin: "https://app.example.com",
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const data =
|
|
107
|
+
operation === "list"
|
|
108
|
+
? await transport.listConversations(args)
|
|
109
|
+
: operation === "create"
|
|
110
|
+
? await transport.createConversation(args)
|
|
111
|
+
: operation === "messages"
|
|
112
|
+
? await transport.listMessages(args)
|
|
113
|
+
: operation === "send"
|
|
114
|
+
? await transport.sendMessage(args)
|
|
115
|
+
: operation === "read"
|
|
116
|
+
? await transport.markRead(args)
|
|
117
|
+
: undefined;
|
|
118
|
+
if (data === undefined)
|
|
119
|
+
return Response.json({ error: "Unknown operation" }, { status: 400 });
|
|
120
|
+
return Response.json({ data });
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Build the identity from your session on every request. Never copy identity fields from the browser request body. Set `origin` from your own config, not from the `Origin` header.
|
|
125
|
+
|
|
126
|
+
`createSupportClient`:
|
|
127
|
+
|
|
128
|
+
- requires an `https:` URL. `http:` is allowed for `localhost`, `127.0.0.1`, `[::1]` and `*.localhost` only. URLs with credentials or a fragment are rejected.
|
|
129
|
+
- sends the key only as `Authorization: Bearer <key>` to that URL.
|
|
130
|
+
- never follows a redirect (`redirect: "manual"`, any 3xx is an error), so the key never reaches another host.
|
|
131
|
+
- times out after 8 seconds and aborts the request.
|
|
132
|
+
- reads at most 2 MiB of response, by declared length and while streaming.
|
|
133
|
+
- validates inputs before the request and the response after it.
|
|
134
|
+
- never retries. Retry from the caller with the same `requestId`. `SupportError.retryable` says when a retry can succeed.
|
|
135
|
+
- never puts the key in an error. It drops the cause of a network error, and redacts the key from a server error message.
|
|
136
|
+
- works with any `fetch`: Node 18+, Cloudflare Workers, Deno, Bun. Pass `fetch` to override.
|
|
137
|
+
|
|
138
|
+
`SupportError.code` is one of `invalid_config`, `invalid_input`, `network`, `timeout`, `redirect`, `too_large`, `invalid_response`, `http`. `http` errors carry `status` and the server's `error` text, cut to 300 characters.
|
|
139
|
+
|
|
140
|
+
## Wire contract
|
|
141
|
+
|
|
142
|
+
Each call is one `POST` to `url`.
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
Authorization: Bearer <key>
|
|
146
|
+
Content-Type: application/json
|
|
147
|
+
|
|
148
|
+
{ "identity": SupportIdentity, "operation": "list" | "create" | "messages" | "send" | "read", ...args }
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
| Method | `operation` | Args | `data` |
|
|
152
|
+
| --------------------------- | ----------- | ------------------------------------- | ------------------------- |
|
|
153
|
+
| `listConversations(args?)` | `list` | `cursor?` | `SupportConversationPage` |
|
|
154
|
+
| `createConversation(input)` | `create` | `body`, `requestId`, `subject?` | `SupportConversation` |
|
|
155
|
+
| `listMessages(input)` | `messages` | `conversationId`, `cursor?` | `SupportPage` |
|
|
156
|
+
| `sendMessage(input)` | `send` | `conversationId`, `body`, `requestId` | `SupportMessage` |
|
|
157
|
+
| `markRead(input)` | `read` | `conversationId`, `messageId` | `SupportReadResult` |
|
|
158
|
+
|
|
159
|
+
Success is `{ "data": result }`. Failure is a non-2xx status with `{ "error": string }`.
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
type SupportConversation = {
|
|
163
|
+
id: string;
|
|
164
|
+
subject: string;
|
|
165
|
+
status: "open" | "closed";
|
|
166
|
+
updatedAt: number;
|
|
167
|
+
preview: string;
|
|
168
|
+
unread: boolean;
|
|
169
|
+
};
|
|
170
|
+
type SupportConversationPage = {
|
|
171
|
+
conversations: SupportConversation[];
|
|
172
|
+
nextCursor: string | null;
|
|
173
|
+
hasUnread: boolean; // across every conversation of the customer
|
|
174
|
+
};
|
|
175
|
+
type SupportReadResult = { hasUnread: boolean };
|
|
176
|
+
type SupportMessage = {
|
|
177
|
+
id: string;
|
|
178
|
+
author: "customer" | "operator";
|
|
179
|
+
body: string;
|
|
180
|
+
createdAt: number;
|
|
181
|
+
};
|
|
182
|
+
type SupportPage = { messages: SupportMessage[]; nextCursor: string | null };
|
|
183
|
+
type SupportIdentity = {
|
|
184
|
+
userId: string;
|
|
185
|
+
accountId: string;
|
|
186
|
+
displayName: string;
|
|
187
|
+
origin: string;
|
|
188
|
+
};
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Limits:
|
|
192
|
+
|
|
193
|
+
| Field | Rule |
|
|
194
|
+
| ---------------------------------------------------- | -------------------------------------------------------------------------- |
|
|
195
|
+
| `body` | trimmed, 1 to 4000 characters |
|
|
196
|
+
| `subject` | optional, trimmed, at most 120 characters. Empty is sent as absent. |
|
|
197
|
+
| `requestId` | 1 to 128 letters, digits, underscores or hyphens. The widget sends a UUID. |
|
|
198
|
+
| `conversationId`, `messageId`, `userId`, `accountId` | 1 to 128 characters |
|
|
199
|
+
| `cursor` | optional, 1 to 16000 characters |
|
|
200
|
+
| `displayName` | trimmed, 1 to 200 characters |
|
|
201
|
+
| `origin` | an `http(s)` origin with no path, for example `https://app.example.com` |
|
|
202
|
+
| response | at most 2 MiB, lists at most 1000 items |
|
|
203
|
+
|
|
204
|
+
The response limit is 2 MiB because JSON writes a control character as six bytes. A page of 50 messages with 4000 control characters each is about 1.2 MiB.
|
|
205
|
+
|
|
206
|
+
Conversation paging: `listConversations` without a cursor returns the 20 newest conversations, newest first. `nextCursor` loads the next older page, and is `null` on the last page.
|
|
207
|
+
|
|
208
|
+
Message paging: `listMessages` without a cursor returns the newest page. `messages` are in ascending order inside a page. `nextCursor` loads the page before it, and is `null` at the start of the conversation.
|
|
209
|
+
|
|
210
|
+
Read state: `read` runs under the same verified identity as every other operation. The server clears unread for the conversation only when `messageId` is still its newest operator message, so a reply that arrives during the call stays unread. `data.hasUnread` is the state across every conversation of the customer after the call, and must be a boolean. `unread` and `hasUnread` in a list response read as `false` when a server omits them. A customer message never changes read state.
|
|
211
|
+
|
|
212
|
+
Idempotency: `create` and `send` with a `requestId` the server already saw for that identity return the first result and store nothing new.
|
|
213
|
+
|
|
214
|
+
## Development
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
pnpm --filter @intray/support check-types
|
|
218
|
+
pnpm --filter @intray/support test
|
|
219
|
+
pnpm --filter @intray/support build
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
The widget tests run in jsdom. The server tests run in the Node environment with real `Response` and `ReadableStream` objects.
|
|
223
|
+
|
|
224
|
+
### Place the launcher in your layout
|
|
225
|
+
|
|
226
|
+
Pass `launcherContainer: HTMLElement` to put the launcher in an existing toolbar or support row. It stays in normal layout flow rather than covering application controls. The conversation panel remains a fixed overlay. The host remains private and uses a closed shadow root; `destroy()` removes only the SDK host, not your container. Omit the option for the floating launcher.
|
|
227
|
+
|
|
228
|
+
### Home, Messages and the first open
|
|
229
|
+
|
|
230
|
+
The panel has two tabs. **Home** shows a greeting, the support team, the expected reply time, a "Send us a message" action and the most recent conversation. **Messages** lists conversations and has a "New conversation" action. "Send us a message" continues the open conversation when there is one, and starts a new conversation otherwise.
|
|
231
|
+
|
|
232
|
+
`widget.open()` continues the open conversation when the widget already knows of one. With `notifications` on, the check on mount supplies that. Otherwise it shows Home. `widget.open("messages")` shows the list. A reopen returns to the view the customer left.
|
|
233
|
+
|
|
234
|
+
Faces come from Intray. Each conversation carries `responder`, the verified profile of the operator who replied last. Rows show it, Home shows the distinct responders of the latest page, and a thread header shows the operators in that thread. A reply from before profile snapshots shows as "Support". Pass `team` to show a fixed set of people on Home instead.
|
|
235
|
+
|
|
236
|
+
Before anyone has replied, the application stands in. The conversation list carries `brand`, the application's name and logo from Live > Manage applications. Home and rows without a reply show it. Photos and logos are served from stable Intray URLs (`/images/<key>` on the Convex site), so a URL copied into a message keeps loading.
|
|
237
|
+
|
|
238
|
+
The widget invents no other copy about your team. Pass it:
|
|
239
|
+
|
|
240
|
+
| Option | Use |
|
|
241
|
+
| -------------- | ------------------------------------------------------------------- |
|
|
242
|
+
| `greeting` | First line of Home. Defaults to "Hi there". |
|
|
243
|
+
| `intro` | Second line of Home. Defaults to "How can we help?". |
|
|
244
|
+
| `team` | Up to four `{ name, avatarUrl? }`. Photos load over HTTPS only. |
|
|
245
|
+
| `replyTime` | Shown on Home and under the conversation heading. Omitted if empty. |
|
|
246
|
+
| `onOpenChange` | Runs with `true` or `false` after the panel opens or closes. |
|
|
247
|
+
|
|
248
|
+
### Placement
|
|
249
|
+
|
|
250
|
+
The default launcher is a circular icon in a floating bottom-right dock. The panel opens above it without reserving space in the product layout.
|
|
251
|
+
|
|
252
|
+
`placement: "side"` docks a full-height panel to the right edge, for products where a floating panel covers the work. Use `onOpenChange` to give the panel 360px of your layout. The floating launcher hides while a side panel is open; pair it with `launcherContainer` to open it from your own toolbar. Below 360px of width the panel fills the screen.
|
|
253
|
+
|
|
254
|
+
`widget.controlsContainer` is a private mount point for the live SDK's control consent prompt; it is removed by `destroy()`. Pass it as the live SDK's `controlsContainer`.
|
|
255
|
+
|
|
256
|
+
Operator messages may include `authorName` and an HTTPS `authorAvatarUrl`. Intray snapshots these from the authenticated responder profile when a reply is sent. Older replies fall back to “Support” and initials. Failed images also fall back to initials; photos load without a referrer.
|
|
257
|
+
|
|
258
|
+
The messenger follows the Agent composer interaction: a compact growing message surface, Enter to send, Shift+Enter for a newline, and an immediate pending bubble. Failed sends restore the draft and preserve the request ID for retry. The customer UI does not ask for a subject. Intray names a conversation by the first line of its first message; the transport still accepts a subject from programmatic callers.
|
|
259
|
+
|
|
260
|
+
The launcher, panel, view changes, new replies and the send button animate. All motion is off under `prefers-reduced-motion`, and the panel then hides at once on close. Otherwise it stays for a 160ms close animation and takes no input during it.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var s=class extends Error{code;status;constructor(r,i,b){super(i),this.name="SupportError",this.code=r,this.status=b}get retryable(){return this.code==="network"||this.code==="timeout"?!0:this.code!=="http"||this.status===void 0?!1:this.status>=500||this.status===429||this.status===408}};var p=4e3,u=120,d=128,f=128,c=16e3,S=1e3,x=/^[A-Za-z0-9_-]+$/;function e(t){return new s("invalid_input",t)}function n(t){return new s("invalid_response",`Support response has an invalid ${t}`)}function T(t){if(typeof t!="string")throw e("body must be a string");let r=t.trim();if(r.length===0)throw e("body is empty");if(r.length>p)throw e(`body is longer than ${p} characters`);return r}function R(t){if(t===void 0)return;if(typeof t!="string")throw e("subject must be a string");let r=t.trim();if(r.length>u)throw e(`subject is longer than ${u} characters`);return r.length===0?void 0:r}function M(t){if(typeof t!="string"||t.length>d||!x.test(t))throw e(`requestId must be 1 to ${d} letters, digits, underscores or hyphens`);return t}function N(t,r){if(typeof t!="string"||t.length===0)throw e(`${r} must be a non-empty string`);if(t.length>f)throw e(`${r} is longer than ${f} characters`);return t}function I(t){if(t!==void 0){if(typeof t!="string"||t.length===0||t.length>c)throw e(`cursor must be 1 to ${c} characters when set`);return t}}function o(t){return typeof t=="object"&&t!==null&&!Array.isArray(t)}function g(t,r){if(typeof t!="string"||t.length===0)throw n(r);return t}function a(t,r){if(typeof t!="string")throw n(r);return t}function h(t,r){if(typeof t!="number"||!Number.isFinite(t))throw n(r);return t}function m(t,r,i){if(!Array.isArray(t)||t.length>S)throw n(r);return t.map(i)}function w(t,r){if(t===void 0)return!1;if(typeof t!="boolean")throw n(r);return t}function k(t){if(!o(t)||typeof t.name!="string")return;let r=t.name.trim().slice(0,200);if(r)return{name:r,...typeof t.logoUrl=="string"&&/^https:\/\//i.test(t.logoUrl)?{logoUrl:t.logoUrl}:{}}}function U(t){if(!o(t)||typeof t.name!="string")return;let r=t.name.trim().slice(0,200);if(r)return{name:r,...typeof t.avatarUrl=="string"&&/^https:\/\//i.test(t.avatarUrl)?{avatarUrl:t.avatarUrl}:{}}}function A(t){if(!o(t))throw n("conversation");if(t.status!=="open"&&t.status!=="closed")throw n("conversation status");let r=U(t.responder);return{id:g(t.id,"conversation id"),subject:a(t.subject,"conversation subject"),status:t.status,updatedAt:h(t.updatedAt,"conversation updatedAt"),preview:a(t.preview,"conversation preview"),unread:w(t.unread,"conversation unread"),...r?{responder:r}:{}}}function y(t,r){if(t!==null&&typeof t!="string")throw n(r);return t}function $(t){if(!o(t))throw n("conversation page");let r=k(t.brand);return{conversations:m(t.conversations,"conversation page conversations",A),nextCursor:y(t.nextCursor,"conversation page nextCursor"),hasUnread:w(t.hasUnread,"conversation page hasUnread"),...r?{brand:r}:{}}}function _(t){if(!o(t))throw n("message");if(t.author!=="customer"&&t.author!=="operator")throw n("message author");return{id:g(t.id,"message id"),author:t.author,...typeof t.authorName=="string"&&t.authorName.trim()?{authorName:t.authorName.trim().slice(0,200)}:{},...typeof t.authorAvatarUrl=="string"&&/^https:\/\//i.test(t.authorAvatarUrl)?{authorAvatarUrl:t.authorAvatarUrl}:{},body:a(t.body,"message body"),createdAt:h(t.createdAt,"message createdAt")}}function j(t){if(!o(t))throw n("page");return{messages:m(t.messages,"page messages",_),nextCursor:y(t.nextCursor,"page nextCursor")}}function L(t){if(!o(t))throw n("read result");if(typeof t.hasUnread!="boolean")throw n("read result hasUnread");return{hasUnread:t.hasUnread}}export{s as a,p as b,u as c,d,T as e,R as f,M as g,N as h,I as i,A as j,$ as k,_ as l,j as m,L as n};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { l as SupportTransport, k as SupportTeamMember } from './types-CLIkWasY.js';
|
|
2
|
+
export { C as CreateConversationInput, L as ListConversationsInput, a as ListMessagesInput, M as MarkReadInput, S as SendMessageInput, b as SupportBrand, c as SupportConversation, d as SupportConversationPage, e as SupportError, f as SupportErrorCode, g as SupportIdentity, h as SupportMessage, i as SupportPage, j as SupportReadResult } from './types-CLIkWasY.js';
|
|
3
|
+
|
|
4
|
+
declare const MAX_BODY_LENGTH = 4000;
|
|
5
|
+
declare const MAX_SUBJECT_LENGTH = 120;
|
|
6
|
+
declare const MAX_REQUEST_ID_LENGTH = 128;
|
|
7
|
+
|
|
8
|
+
type SupportWidgetOptions = {
|
|
9
|
+
transport: SupportTransport;
|
|
10
|
+
title?: string;
|
|
11
|
+
/** Mount the launcher in host layout instead of floating over product controls. */
|
|
12
|
+
launcherContainer?: HTMLElement;
|
|
13
|
+
/**
|
|
14
|
+
* `"side"` docks a full-height panel to the right edge, for products where a
|
|
15
|
+
* floating panel covers the work. Defaults to `"floating"`.
|
|
16
|
+
*/
|
|
17
|
+
placement?: "floating" | "side";
|
|
18
|
+
/** First line of the home view. Defaults to "Hi there". */
|
|
19
|
+
greeting?: string;
|
|
20
|
+
/** Second line of the home view. Defaults to "How can we help?". */
|
|
21
|
+
intro?: string;
|
|
22
|
+
/** People shown on the home view. Photos load over HTTPS only. */
|
|
23
|
+
team?: SupportTeamMember[];
|
|
24
|
+
/** Host copy for the expected reply time, for example "We usually reply in a few minutes". */
|
|
25
|
+
replyTime?: string;
|
|
26
|
+
/** Runs after the panel opens or closes, so the host can resize its layout. */
|
|
27
|
+
onOpenChange?: (open: boolean) => void;
|
|
28
|
+
/**
|
|
29
|
+
* Shows an unread indicator on the closed launcher. The closed widget then
|
|
30
|
+
* checks for unread replies while the tab is visible. Defaults to true.
|
|
31
|
+
* `false` makes no request while the widget is closed.
|
|
32
|
+
*/
|
|
33
|
+
notifications?: boolean;
|
|
34
|
+
/** Receives every transport error. The widget shows its own fixed copy. */
|
|
35
|
+
onError?: (error: unknown) => void;
|
|
36
|
+
};
|
|
37
|
+
type SupportWidget = {
|
|
38
|
+
/** Private dock for live sharing status and consent controls; removed on destroy. */
|
|
39
|
+
controlsContainer: HTMLElement;
|
|
40
|
+
/**
|
|
41
|
+
* Without a target, the first open continues the open conversation when one
|
|
42
|
+
* is known, and shows the home view otherwise. `"messages"` shows the
|
|
43
|
+
* conversation list.
|
|
44
|
+
*/
|
|
45
|
+
open(target?: "messages"): void;
|
|
46
|
+
close(): void;
|
|
47
|
+
destroy(): void;
|
|
48
|
+
};
|
|
49
|
+
declare function createSupportWidget(options: SupportWidgetOptions): SupportWidget;
|
|
50
|
+
|
|
51
|
+
export { MAX_BODY_LENGTH, MAX_REQUEST_ID_LENGTH, MAX_SUBJECT_LENGTH, SupportTeamMember, SupportTransport, type SupportWidget, type SupportWidgetOptions, createSupportWidget };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import{a as ut,b as Be,c as Tn,d as En,j as nn,k as on,l as rn,m as De,n as an}from"./chunk-3IC2MERJ.js";var Ln=15e3;function sn(){let a=new Set,l=!1;function u(y){return l?Promise.reject(new Error("Support widget was destroyed")):new Promise((Ve,N)=>{let Y=()=>{clearTimeout(g),a.delete(f)},f=()=>{Y(),N(new Error("Support widget was destroyed"))},g=setTimeout(()=>{Y(),N(new ut("timeout","Support transport timed out"))},Ln);a.add(f),(async()=>y())().then(i=>{Y(),Ve(i)},i=>{Y(),N(i)})})}function d(){l=!0;for(let y of[...a])y()}return{run:u,cancelAll:d}}var dn=`
|
|
2
|
+
:host { all: initial; }
|
|
3
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
4
|
+
[hidden] { display: none !important; }
|
|
5
|
+
.root {
|
|
6
|
+
--spring: cubic-bezier(.2, .9, .25, 1.12);
|
|
7
|
+
--ease: cubic-bezier(.2, .8, .2, 1);
|
|
8
|
+
position: fixed; right: 20px; bottom: 20px;
|
|
9
|
+
display: flex; flex-direction: column; align-items: flex-end; gap: 12px;
|
|
10
|
+
max-width: calc(100vw - 24px);
|
|
11
|
+
color-scheme: light;
|
|
12
|
+
font: 14px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
13
|
+
color: #1a1a1a;
|
|
14
|
+
}
|
|
15
|
+
button, textarea { font: inherit; color: inherit; }
|
|
16
|
+
button { all: unset; box-sizing: border-box; cursor: pointer; font: inherit; }
|
|
17
|
+
button:focus-visible, textarea:focus-visible {
|
|
18
|
+
outline: 2px solid #202020; outline-offset: 2px;
|
|
19
|
+
}
|
|
20
|
+
button[disabled] { cursor: default; opacity: .55; }
|
|
21
|
+
.sr {
|
|
22
|
+
position: absolute; width: 1px; height: 1px; overflow: hidden;
|
|
23
|
+
clip-path: inset(50%); white-space: nowrap;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.launcher {
|
|
27
|
+
position: relative; display: flex; align-items: center; justify-content: center;
|
|
28
|
+
width: 56px; height: 56px; border-radius: 50%; flex: none;
|
|
29
|
+
background: #202020; color: #fff;
|
|
30
|
+
box-shadow: 0 1px 2px rgba(0,0,0,.08), 0 8px 28px rgba(0,0,0,.18);
|
|
31
|
+
}
|
|
32
|
+
.launcher:hover { background: #383838; }
|
|
33
|
+
.launcher-icon { position: relative; width: 26px; height: 26px; }
|
|
34
|
+
.launcher-icon svg { position: absolute; inset: 0; }
|
|
35
|
+
.launcher-icon .icon-close { opacity: 0; }
|
|
36
|
+
.launcher-icon.is-open .icon-chat { opacity: 0; }
|
|
37
|
+
.launcher-icon.is-open .icon-close { opacity: 1; }
|
|
38
|
+
.badge { position: absolute; top: 2px; right: 2px; width: 12px; height: 12px; border-radius: 50%; background: #202020; border: 2px solid white; }
|
|
39
|
+
:host(:not([data-inline])) .root[data-placement="side"][data-open] .launcher { display: none; }
|
|
40
|
+
|
|
41
|
+
.live-controls { display: flex; flex-direction: column; gap: 6px; max-width: min(360px, calc(100vw - 24px)); background: white; border-radius: 16px; padding: 4px; box-shadow: 0 1px 2px rgba(0,0,0,.06), 0 8px 28px rgba(0,0,0,.12); }
|
|
42
|
+
.live-controls:empty { display: none; }
|
|
43
|
+
|
|
44
|
+
.panel {
|
|
45
|
+
width: min(372px, calc(100vw - 24px));
|
|
46
|
+
height: min(560px, calc(100dvh - 120px)); min-height: 160px;
|
|
47
|
+
display: flex; flex-direction: column; overflow: hidden;
|
|
48
|
+
background: #fff; border-radius: 20px;
|
|
49
|
+
box-shadow: 0 1px 2px rgba(0,0,0,.06), 0 12px 48px rgba(0,0,0,.16);
|
|
50
|
+
}
|
|
51
|
+
:host([data-inline]) .root { position: static; }
|
|
52
|
+
:host([data-inline]) .panel { position: fixed; right: 20px; bottom: 88px; }
|
|
53
|
+
.root[data-placement="side"] .panel,
|
|
54
|
+
:host([data-inline]) .root[data-placement="side"] .panel {
|
|
55
|
+
position: fixed; top: 0; right: 0; bottom: 0;
|
|
56
|
+
width: min(360px, 100vw); height: auto; border-radius: 0;
|
|
57
|
+
border-left: 1px solid #ececec; box-shadow: -12px 0 48px rgba(0,0,0,.06);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.header {
|
|
61
|
+
display: flex; align-items: center; gap: 8px; min-height: 56px;
|
|
62
|
+
padding: 10px 12px 10px 16px; background: #fff;
|
|
63
|
+
}
|
|
64
|
+
.header[data-index] { padding-left: 20px; }
|
|
65
|
+
.titles { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
|
66
|
+
.heading {
|
|
67
|
+
margin: 0; font-size: 15px; font-weight: 600;
|
|
68
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
69
|
+
}
|
|
70
|
+
.subheading { margin: 0; color: #777777; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
71
|
+
.icon-button { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; flex: none; border-radius: 50%; color: #737373; }
|
|
72
|
+
.icon-button:hover:not([disabled]) { background: #f2f2f2; color: #171717; }
|
|
73
|
+
.icon-button.send-button { width: 32px; height: 32px; color: white; background: #202020; }
|
|
74
|
+
.icon-button.send-button:hover:not([disabled]) { background: #383838; color: white; }
|
|
75
|
+
.text-button { padding: 6px 10px; border-radius: 999px; color: #202020; font-weight: 500; }
|
|
76
|
+
.text-button:hover:not([disabled]) { background: #eeeeee; }
|
|
77
|
+
.notice { margin: 0; padding: 8px 16px; background: #fafafa; color: #555555; font-size: 13px; }
|
|
78
|
+
.main { position: relative; flex: 1; min-height: 0; display: flex; flex-direction: column; }
|
|
79
|
+
.body { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 12px 16px; }
|
|
80
|
+
.view { display: flex; flex-direction: column; gap: 8px; min-height: 100%; }
|
|
81
|
+
ul, ol { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
|
|
82
|
+
|
|
83
|
+
.home { gap: 10px; }
|
|
84
|
+
.hero { display: flex; flex-direction: column; gap: 12px; margin: -12px -16px 4px; padding: 20px 20px 16px; background: linear-gradient(180deg, #f3f3f1, #fff); }
|
|
85
|
+
.faces { display: flex; }
|
|
86
|
+
.face { position: relative; display: flex; align-items: center; justify-content: center; width: 36px; height: 36px; margin-left: -8px; border-radius: 50%; overflow: hidden; border: 2px solid #fff; background: #e7e4de; color: #5c564d; font-size: 12px; font-weight: 600; flex: none; }
|
|
87
|
+
.face:first-child { margin-left: 0; }
|
|
88
|
+
.faces[hidden] + .greeting { margin-top: 0; }
|
|
89
|
+
.header-faces { flex: none; }
|
|
90
|
+
.header-face { width: 30px; height: 30px; font-size: 10px; }
|
|
91
|
+
.row-face { grid-row: 1 / span 2; width: 32px; height: 32px; margin: 0; border: 0; font-size: 11px; color: #8a857c; }
|
|
92
|
+
.face img, .avatar img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
|
|
93
|
+
.greeting { margin: 0; font-size: 24px; line-height: 1.15; font-weight: 600; letter-spacing: -.4px; color: #202020; }
|
|
94
|
+
.intro { margin: 0; font-size: 24px; line-height: 1.15; font-weight: 600; letter-spacing: -.4px; color: #8a8a8a; }
|
|
95
|
+
.card { width: 100%; border-radius: 14px; background: #fff; box-shadow: 0 0 0 1px #ececec, 0 2px 8px rgba(0,0,0,.04); }
|
|
96
|
+
button.card:hover { box-shadow: 0 0 0 1px #e2e2e2, 0 6px 18px rgba(0,0,0,.09); }
|
|
97
|
+
.start { display: flex; align-items: center; gap: 12px; padding: 16px; }
|
|
98
|
+
.start-text { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
|
99
|
+
.start-title { font-weight: 600; }
|
|
100
|
+
.start-sub { color: #777777; font-size: 13px; }
|
|
101
|
+
.start-go { display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 50%; background: #202020; color: #fff; flex: none; }
|
|
102
|
+
.card-label { margin: 0; padding: 12px 16px 2px; color: #777777; font-size: 12px; font-weight: 500; }
|
|
103
|
+
.recent .conversation { border-radius: 0 0 14px 14px; padding: 8px 16px 14px; }
|
|
104
|
+
|
|
105
|
+
.conversation {
|
|
106
|
+
display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 1px 10px;
|
|
107
|
+
width: 100%; padding: 10px 12px; border-radius: 12px;
|
|
108
|
+
}
|
|
109
|
+
.conversation:hover { background: #f6f6f6; }
|
|
110
|
+
.subject { font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
111
|
+
.conversation[data-unread] .subject { font-weight: 600; }
|
|
112
|
+
.conversation[data-unread] .meta::after {
|
|
113
|
+
content: ""; display: inline-block; width: 8px; height: 8px;
|
|
114
|
+
margin-left: 8px; border-radius: 50%; background: #202020;
|
|
115
|
+
}
|
|
116
|
+
.meta { color: #777777; font-size: 12px; white-space: nowrap; }
|
|
117
|
+
.preview {
|
|
118
|
+
grid-column: 2 / -1; color: #666666; font-size: 13px;
|
|
119
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
120
|
+
}
|
|
121
|
+
.list-foot { position: sticky; bottom: 0; margin-top: auto; display: flex; justify-content: center; padding: 8px 0 4px; pointer-events: none; }
|
|
122
|
+
.primary {
|
|
123
|
+
pointer-events: auto; padding: 10px 20px; border-radius: 999px; text-align: center;
|
|
124
|
+
background: #202020; color: #fff; font-weight: 500;
|
|
125
|
+
box-shadow: 0 1px 2px rgba(0,0,0,.08), 0 8px 24px rgba(0,0,0,.16);
|
|
126
|
+
}
|
|
127
|
+
.primary:hover:not([disabled]) { background: #383838; }
|
|
128
|
+
.empty, .hint { margin: 0; color: #777777; }
|
|
129
|
+
.hint { font-size: 12px; }
|
|
130
|
+
.welcome { justify-content: center; min-height: 240px; padding: 24px 12px; }
|
|
131
|
+
.welcome-title { margin: 0; color: #202020; font-size: 24px; line-height: 1.2; font-weight: 600; letter-spacing: -.6px; }
|
|
132
|
+
.welcome .empty { max-width: 240px; line-height: 1.6; }
|
|
133
|
+
|
|
134
|
+
.tabs { display: flex; flex: none; height: 58px; border-top: 1px solid #efefef; background: #fff; }
|
|
135
|
+
.tab { position: relative; flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; color: #8a8a8a; font-size: 12px; font-weight: 500; }
|
|
136
|
+
.tab:hover { color: #505050; }
|
|
137
|
+
.tab[aria-current="page"] { color: #202020; }
|
|
138
|
+
.tab-dot { position: absolute; top: 10px; left: calc(50% + 8px); width: 8px; height: 8px; border-radius: 50%; background: #202020; border: 2px solid #fff; box-sizing: content-box; }
|
|
139
|
+
|
|
140
|
+
.messages { gap: 16px; }
|
|
141
|
+
.message.grouped { margin-top: -8px; }
|
|
142
|
+
.message { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; }
|
|
143
|
+
.message.operator { position: relative; padding-left: 36px; }
|
|
144
|
+
.avatar { position: absolute; left: 0; top: 20px; display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: 50%; overflow: hidden; background: #e7e4de; color: #5c564d; font-size: 10px; font-weight: 600; }
|
|
145
|
+
.author-name { font-size: 11px; color: #737373; margin-left: 4px; }
|
|
146
|
+
.message.customer { align-items: flex-end; }
|
|
147
|
+
.bubble {
|
|
148
|
+
margin: 0; max-width: 85%; padding: 9px 13px; border-radius: 18px;
|
|
149
|
+
background: #f2f2f2; white-space: pre-wrap; overflow-wrap: anywhere;
|
|
150
|
+
}
|
|
151
|
+
.message.customer .bubble { background: #202020; color: #fff; border-bottom-right-radius: 6px; }
|
|
152
|
+
.message.operator .bubble { border-bottom-left-radius: 6px; }
|
|
153
|
+
.message time { color: #999; font-size: 10px; margin-top: 3px; }
|
|
154
|
+
.older { align-self: center; }
|
|
155
|
+
.list-nav { display: flex; gap: 8px; }
|
|
156
|
+
.list-nav > :last-child { margin-left: auto; }
|
|
157
|
+
.status:not(.sr) {
|
|
158
|
+
position: absolute; top: 8px; left: 50%; transform: translateX(-50%); z-index: 1;
|
|
159
|
+
margin: 0; padding: 7px 14px; width: max-content; max-width: calc(100% - 32px);
|
|
160
|
+
border-radius: 14px; background: #fff; color: #555555; font-size: 13px; text-align: center;
|
|
161
|
+
box-shadow: 0 1px 2px rgba(0,0,0,.08), 0 8px 24px rgba(0,0,0,.14);
|
|
162
|
+
}
|
|
163
|
+
.status:empty { display: none; }
|
|
164
|
+
.status[data-error] { color: #a3261c; }
|
|
165
|
+
.skeleton { display: flex; flex-direction: column; gap: 18px; padding: 10px 12px; }
|
|
166
|
+
.skeleton span { display: block; height: 34px; border-radius: 10px; background: #f3f3f3; }
|
|
167
|
+
.skeleton span:nth-child(2) { width: 82%; }
|
|
168
|
+
.skeleton span:nth-child(3) { width: 64%; }
|
|
169
|
+
.composer { position: relative; display: flex; align-items: flex-end; gap: 8px; margin: 8px 12px 12px; padding: 6px 6px 6px 16px; border-radius: 22px; background: #f4f4f4; }
|
|
170
|
+
.field {
|
|
171
|
+
flex: 1; min-width: 0; border: 0; background: transparent; resize: none;
|
|
172
|
+
min-height: 24px; max-height: 144px; margin: 4px 0; padding: 0; line-height: 24px; overflow-y: auto;
|
|
173
|
+
}
|
|
174
|
+
.composer:focus-within { box-shadow: 0 0 0 2px #20202026; }
|
|
175
|
+
.composer .field:focus-visible { outline: none; }
|
|
176
|
+
.actions { display: flex; flex: none; }
|
|
177
|
+
.pending-message { margin-top: 12px; opacity: .7; }
|
|
178
|
+
.delivery { font-size: 11px; color: #777; }
|
|
179
|
+
|
|
180
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
181
|
+
.launcher { transition: transform .2s var(--spring), background-color .12s; }
|
|
182
|
+
.launcher:hover { transform: scale(1.06); }
|
|
183
|
+
.launcher:active { transform: scale(.9); transition-duration: .08s; }
|
|
184
|
+
.launcher-icon svg { transition: opacity .18s, transform .28s var(--spring); }
|
|
185
|
+
.launcher-icon .icon-close { transform: rotate(-60deg) scale(.6); }
|
|
186
|
+
.launcher-icon.is-open .icon-chat { transform: rotate(60deg) scale(.6); }
|
|
187
|
+
.launcher-icon.is-open .icon-close { transform: none; }
|
|
188
|
+
.badge { animation: pop .3s var(--spring); }
|
|
189
|
+
.status:not(.sr) { animation: toast-in .24s var(--ease); }
|
|
190
|
+
.skeleton span { animation: pulse 1.2s ease-in-out infinite; }
|
|
191
|
+
.skeleton span:nth-child(2) { animation-delay: .12s; }
|
|
192
|
+
.skeleton span:nth-child(3) { animation-delay: .24s; }
|
|
193
|
+
|
|
194
|
+
.panel { transform-origin: bottom right; }
|
|
195
|
+
.panel[data-state="open"] { animation: panel-in .34s var(--spring); }
|
|
196
|
+
.panel[data-state="closing"] { animation: panel-out 160ms ease-in forwards; }
|
|
197
|
+
.root[data-placement="side"] .panel[data-state="open"] { animation: side-in .32s var(--ease); }
|
|
198
|
+
.root[data-placement="side"] .panel[data-state="closing"] { animation: side-out 160ms ease-in forwards; }
|
|
199
|
+
|
|
200
|
+
.view[data-enter="forward"] { animation: view-forward .26s var(--ease); }
|
|
201
|
+
.view[data-enter="back"] { animation: view-back .26s var(--ease); }
|
|
202
|
+
.view[data-enter="fade"] { animation: view-fade .2s ease-out; }
|
|
203
|
+
.view[data-enter] .rise { animation: rise .42s var(--ease) both; animation-delay: calc(var(--i, 0) * 50ms + 60ms); }
|
|
204
|
+
.message.new .bubble, .pending-message .bubble { animation: pop .3s var(--spring); }
|
|
205
|
+
.message.operator.new .bubble { transform-origin: bottom left; }
|
|
206
|
+
.message.customer .bubble { transform-origin: bottom right; }
|
|
207
|
+
|
|
208
|
+
.composer { transition: box-shadow .15s; }
|
|
209
|
+
.send-button { transition: transform .2s var(--spring), background-color .12s; }
|
|
210
|
+
.send-button:active:not([disabled]) { transform: scale(.9); }
|
|
211
|
+
.card { transition: box-shadow .15s, transform .15s; }
|
|
212
|
+
button.card:hover { transform: translateY(-1px); }
|
|
213
|
+
.primary { transition: transform .2s var(--spring), background-color .12s; }
|
|
214
|
+
.primary:hover:not([disabled]) { transform: scale(1.03); }
|
|
215
|
+
.primary:active:not([disabled]) { transform: scale(.96); }
|
|
216
|
+
.text-button, .conversation, .icon-button, .tab { transition: background-color .12s, color .12s; }
|
|
217
|
+
|
|
218
|
+
@keyframes panel-in { from { opacity: 0; transform: translateY(16px) scale(.92); } to { opacity: 1; transform: none; } }
|
|
219
|
+
@keyframes panel-out { to { opacity: 0; transform: translateY(16px) scale(.92); } }
|
|
220
|
+
@keyframes side-in { from { transform: translateX(100%); } to { transform: none; } }
|
|
221
|
+
@keyframes side-out { to { transform: translateX(100%); } }
|
|
222
|
+
@keyframes view-forward { from { opacity: 0; transform: translateX(18px); } to { opacity: 1; transform: none; } }
|
|
223
|
+
@keyframes view-back { from { opacity: 0; transform: translateX(-18px); } to { opacity: 1; transform: none; } }
|
|
224
|
+
@keyframes view-fade { from { opacity: 0; } to { opacity: 1; } }
|
|
225
|
+
@keyframes rise { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
|
|
226
|
+
@keyframes toast-in { from { opacity: 0; transform: translate(-50%, -6px); } to { opacity: 1; transform: translateX(-50%); } }
|
|
227
|
+
@keyframes pulse { 50% { opacity: .5; } }
|
|
228
|
+
@keyframes pop { from { opacity: 0; transform: translateY(8px) scale(.9); } to { opacity: 1; transform: none; } }
|
|
229
|
+
}
|
|
230
|
+
`;var On="data-intray-support",ln=5e3,ft=6e4,_n=15e3,gt=500,In=20,Nn=50,Hn=50,pn="Contact support",zn="Contact support - new messages",Rn=3,Pn="new",Un="m14 6-6 6 6 6",cn="m6 9 6 6 6-6",Bn="M12 19V5m-6 6 6-6 6 6",Dn="M5 12h14m-6-6 6 6-6 6",ht="M6 4h12a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-6l-4.5 3.5V17H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z",Fn="m4 11 8-7 8 7v8a1 1 0 0 1-1 1h-4v-6H9v6H5a1 1 0 0 1-1-1z";function Vn(){if(typeof crypto.randomUUID=="function")return crypto.randomUUID();let a=crypto.getRandomValues(new Uint8Array(16));a[6]=(a[6]??0)&15|64,a[8]=(a[8]??0)&63|128;let l=Array.from(a,u=>u.toString(16).padStart(2,"0")).join("");return`${l.slice(0,8)}-${l.slice(8,12)}-${l.slice(12,16)}-${l.slice(16,20)}-${l.slice(20)}`}function o(a,l,u){let d=document.createElement(a);return l&&(d.className=l),u!==void 0&&(d.textContent=u),d}function O(a,l,u){let d=o("button",a,l);return d.type="button",d.dataset.part=u,d}function X(a,l=18,u){let d=document.createElementNS("http://www.w3.org/2000/svg","svg");d.setAttribute("viewBox","0 0 24 24"),d.setAttribute("width",String(l)),d.setAttribute("height",String(l)),d.setAttribute("fill","none"),d.setAttribute("stroke","currentColor"),d.setAttribute("stroke-width","1.8"),d.setAttribute("stroke-linecap","round"),d.setAttribute("stroke-linejoin","round"),d.setAttribute("aria-hidden","true"),u&&d.setAttribute("class",u);let y=document.createElementNS("http://www.w3.org/2000/svg","path");return y.setAttribute("d",a),d.append(y),d}function mt(a,l,u){let d=O("icon-button","",l);return d.title=a,d.setAttribute("aria-label",a),d.append(o("span","sr",a),X(u)),d}function jn(a){return a.split(/\s+/).slice(0,2).map(l=>l[0]).join("").toUpperCase()}function Fe(a,l,u){let d=o("span",a,jn(l));if(d.setAttribute("aria-hidden","true"),u&&/^https:\/\//i.test(u)){let y=o("img");y.alt="",y.referrerPolicy="no-referrer",y.src=u,y.addEventListener("error",()=>y.remove()),d.append(y)}return d}function un(a){let l=new Date(a),u=l.toDateString()===new Date().toDateString();return new Intl.DateTimeFormat(void 0,u?{hour:"numeric",minute:"2-digit"}:{month:"short",day:"numeric",hour:"numeric",minute:"2-digit"}).format(l)}function $n(a){if(typeof document>"u")throw new Error("createSupportWidget needs a browser document");let{transport:l,onError:u}=a,d=a.title?.trim()||"Support",y=a.notifications!==!1,Ve=a.placement==="side"?"side":"floating",N=a.replyTime?.trim()||"",Y=typeof matchMedia=="function"&&!matchMedia("(prefers-reduced-motion: reduce)").matches,f=!1,g=!1,i="home",xt="home",Q=null,Z,k=[],C=null,K=null,ee=!1,p=null,h=[],R=null,xe=0,te=!1,T=!1,P=!1,E=!1,x=null,L=0,ne,oe=0,je=0,$e=!1,We="",re=null,Xe=new Set,bt="",yt="",H,vt=null,wt="",be=!1,G=0,ie,ae=0,Ye=!1,Ke=!1,se=0,ye=0,U=new Map,_=new Map,kt=sn(),B=kt.run,M=o("div","rr-block ph-no-capture");M.setAttribute(On,""),M.setAttribute("data-private",""),M.setAttribute("data-live-block",""),M.style.cssText="position:fixed;right:0;bottom:0;width:0;height:0;overflow:visible;z-index:2147483000;",a.launcherContainer&&(M.style.cssText="display:block;position:relative;z-index:2147483000;",M.setAttribute("data-inline",""));let ve=M.attachShadow({mode:"closed"}),St=o("style");St.textContent=dn;let de=o("div","root");de.dataset.placement=Ve;let I=O("launcher","","launcher"),le=o("span","badge");le.dataset.part="unread-badge",le.setAttribute("aria-hidden","true"),le.hidden=!0;let fn=o("span","sr",pn),we=o("span","launcher-icon");we.setAttribute("aria-hidden","true"),we.append(X(ht,26,"icon-chat"),X(cn,26,"icon-close")),I.append(fn,we,le),I.setAttribute("aria-haspopup","dialog"),I.setAttribute("aria-expanded","false");let S=o("section","panel");S.dataset.part="panel",S.setAttribute("role","dialog"),S.setAttribute("aria-label",d),S.hidden=!0;let Ge=o("div","header"),qe=mt("Back","back",Un),Ct=o("div","titles"),Je=o("h2","heading");Je.dataset.part="heading";let At=o("p","subheading",N);Ct.append(Je,At);let Tt=o("span","faces header-faces"),Et=mt("Close","close",cn);Ge.append(qe,Tt,Ct,Et);let Qe=o("p","notice","This conversation is closed. Send a message to reopen it.");Qe.dataset.part="closed-notice";let v=o("div","body"),Ze=o("div","view home"),et=o("div","hero"),Lt=(a.team??[]).filter(e=>e.name.trim()).map(e=>({...e,name:e.name.trim()})),Mt=o("div","faces rise");et.append(Mt);let Ot=o("h3","greeting rise",a.greeting?.trim()||"Hi there");Ot.style.setProperty("--i","1");let _t=o("p","intro rise",a.intro?.trim()||"How can we help?");_t.style.setProperty("--i","2"),et.append(Ot,_t);let pe=O("card start rise","","start");pe.style.setProperty("--i","3");let tt=o("span","start-text");tt.append(o("span","start-title","Send us a message")),N&&tt.append(o("span","start-sub",N));let It=o("span","start-go");It.append(X(Dn,16)),pe.append(tt,It);let D=o("div","card recent rise");D.dataset.part="recent",D.style.setProperty("--i","4"),Ze.append(et,pe,D);let nt=o("div","view"),ot=O("primary","New conversation","new"),Nt=o("div","list-foot");Nt.append(ot);let ke=o("ul");ke.dataset.part="conversations";let rt=o("p","empty"),it=o("div","list-nav"),Se=O("text-button","Latest conversations","latest-conversations"),Ce=O("text-button","Older conversations","older-conversations");it.append(Se,Ce),nt.append(rt,ke,it,Nt);let at=o("div","view"),ce=O("text-button older","Load older messages","older"),Ht=o("p","hint","Older messages are not shown here."),ue=o("div");ue.setAttribute("role","log"),ue.setAttribute("aria-label","Messages");let Ae=o("ol","messages");Ae.dataset.part="messages",ue.append(Ae),at.append(ce,Ht,ue);let Te=o("div","view");Te.classList.add("welcome"),Te.append(o("h3","welcome-title","How can we help?"),o("p","empty","Ask a question or tell us what you\u2019re working on."));let F=o("div","message customer pending-message");F.setAttribute("role","status"),F.hidden=!0;let zt=null,Ee=o("div","skeleton");Ee.setAttribute("aria-hidden","true"),Ee.append(o("span"),o("span"),o("span")),v.append(Ze,nt,at,Te,Ee,F);let V=o("p","status");V.dataset.part="status",V.setAttribute("role","status"),V.setAttribute("aria-live","polite");let q=o("form","composer");q.noValidate=!0;let Rt=o("label","sr","Message"),w=o("textarea","field");w.id="message",w.rows=1,w.placeholder="Write a message",w.dataset.part="body",Rt.htmlFor=w.id;let Pt=o("div","actions"),gn=o("span","sr","Enter to send \xB7 Shift + Enter for a new line"),z=mt("Send","send",Bn);z.classList.add("send-button"),z.type="submit",z.dataset.part="send",Pt.append(gn,z),q.append(Rt,w,Pt);let Le=o("nav","tabs");Le.setAttribute("aria-label","Support sections");let Me=O("tab","","tab-home");Me.append(X(Fn,20),o("span",void 0,"Home"));let Oe=O("tab","","tab-messages"),st=o("span","tab-dot");st.setAttribute("aria-hidden","true"),Oe.append(X(ht,20),o("span",void 0,"Messages"),st),Le.append(Me,Oe);let Ut=o("div","main");Ut.append(v,V),S.append(Ge,Qe,Ut,q,Le);let dt=o("div","live-controls");dt.dataset.part="live-controls",de.append(S,dt,I),ve.append(St,de);let A=e=>f||e!==L;function j(e){try{u?.(e)}catch{}}function Bt(e){let n=_.get(e);return n||(n={body:"",attempt:null},_.set(e,n),_.size>Nn&&_.delete(_.keys().next().value)),n}let fe=()=>i==="thread"&&p?p.id:Pn;function Dt(e){k=[...e].sort((t,r)=>r.updatedAt-t.updatedAt);let n=p&&k.find(t=>t.id===p?.id);n&&(p=n)}async function _e(e){return on(await B(()=>l.listConversations(e===null?void 0:{cursor:e})))}function Ft(e){G+=1,be=e}function Ie(e,n,t){n.brand&&(H=n.brand),t===G&&Ft(n.hasUnread),e===C&&(Dt(n.conversations),K=n.nextCursor,ee=!0)}function Vt(e){let n=k.some(t=>t.id===e.id);if(C!==null&&!n){p?.id===e.id&&(p=e);return}Dt([e,...k.filter(t=>t.id!==e.id)])}function jt(){h.length<=gt||(h=h.slice(-gt),R=null,te=!0)}function $t(e){let n=new Map(h.map(t=>[t.id,t]));for(let t of e)n.set(t.id,t);h=[...n.values()].sort((t,r)=>t.createdAt-r.createdAt),jt()}function Wt(e){let n=new Set(h.map(r=>r.id)),t=e.messages.some(r=>n.has(r.id));if(h.length===0||!t&&e.nextCursor!==null){h=e.messages,R=e.nextCursor,xe=0,te=!1,jt();return}$t(e.messages)}function Xt(e,n){let t=O("conversation","",n);t.dataset.id=e.id;let{responder:r}=e;if(r)t.append(Fe("face row-face",r.name,r.avatarUrl),o("span","sr",`Answered by ${r.name}.`));else if(H)t.append(Fe("face row-face",H.name,H.logoUrl));else{let c=o("span","face row-face");c.setAttribute("aria-hidden","true"),c.append(X(ht,14)),t.append(c)}e.unread&&(t.dataset.unread="",t.append(o("span","sr","Unread.")));let s=e.subject||"Conversation",m=(e.status==="closed"?"Closed \xB7 ":"")+un(e.updatedAt);return t.append(o("span","subject",s),o("span","meta",m)),e.preview.startsWith(s)||t.append(o("span","preview",e.preview)),t.addEventListener("click",()=>{he(e)}),t}function hn(){let e=JSON.stringify([k,H]);if(e===bt)return;bt=e;let n=ve.activeElement?.dataset.id;if(ke.replaceChildren(...k.map(t=>{let r=o("li");return r.append(Xt(t,"conversation")),r})),n!==void 0)for(let t of ke.querySelectorAll("button"))t.dataset.id===n&&t.focus()}function Yt(e,n){let t=new Set;return e.filter(r=>{let s=`${r.name}|${r.avatarUrl??""}`;return t.has(s)?!1:(t.add(s),!0)}).slice(0,n)}function Kt(e,n,t){e.hidden=n.length===0,e.replaceChildren(...n.map(r=>Fe(t,r.name,r.avatarUrl)))}function mn(){let e=Lt.length>0?Lt.slice(0,4):Yt(k.flatMap(t=>t.responder?[t.responder]:[]),3);e.length===0&&H&&(e=[{name:H.name,avatarUrl:H.logoUrl}]);let n=JSON.stringify(e);n!==vt&&(vt=n,Kt(Mt,e,"face"))}function xn(){let e=i==="thread"?Yt(h.filter(t=>t.author==="operator").reverse().map(t=>({name:t.authorName||"Support",avatarUrl:t.authorAvatarUrl})),2):[],n=JSON.stringify(e);n!==wt&&(wt=n,Kt(Tt,e,"face header-face"))}function bn(){let e=C===null?k[0]:void 0;D.hidden=!e;let n=JSON.stringify(e??null);if(n===yt)return;yt=n;let t=D.contains(ve.activeElement);if(D.replaceChildren(),!e)return;let r=Xt(e,"recent-conversation");D.append(o("p","card-label","Recent"),r),t&&r.focus()}function yn(){let e=h.map(t=>`${t.id}:${t.createdAt}:${t.body.length}:${t.authorName}:${t.authorAvatarUrl}`).join("|");if(e===We)return!1;We=e,re=null;for(let t of h)t.author==="operator"&&(re=t.id);let n=Xe;return Xe=new Set(h.map(t=>t.id)),Ae.replaceChildren(...h.map((t,r)=>{let s=o("li",`message ${t.author}`);s.dataset.id=t.id,n.size>0&&t.author==="operator"&&!n.has(t.id)&&s.classList.add("new");let m=o("time",void 0,un(t.createdAt));m.dateTime=new Date(t.createdAt).toISOString();let c=h[r-1],W=c?.author===t.author&&c.authorName===t.authorName&&c.authorAvatarUrl===t.authorAvatarUrl&&t.createdAt-c.createdAt<18e4;if(s.classList.toggle("grouped",W),t.author==="operator"&&!W){let me=t.authorName||"Support";s.append(Fe("avatar",me,t.authorAvatarUrl),o("span","author-name",me))}return s.append(o("span","sr",t.author==="customer"?"You:":""),o("p","bubble",t.body),m),s})),!0}function b(){if(f)return;I.hidden=!1,we.classList.toggle("is-open",g),a.launcherContainer&&(M.style.zIndex=g?"2147483000":"auto"),I.setAttribute("aria-expanded",String(g));let e=y&&be;le.hidden=!e,I.setAttribute("aria-label",e?zn:pn),de.toggleAttribute("data-open",g),g&&(clearTimeout(Z),Z=void 0,S.hidden=!1,S.inert=!1,S.dataset.state="open");let n=i==="home"||i==="list";E||F.replaceChildren(),F.hidden=!E||zt!==fe()||n;let t={home:Ze,list:nt,thread:at,new:Te};for(let[m,c]of Object.entries(t))c.hidden=m!==i;if(i!==Q){let m=t[i],c=Q==="home"||Q==="list";for(let W of Object.values(t))delete W.dataset.enter;m.offsetWidth,m.dataset.enter=Q===null||n===c?"fade":n?"back":"forward",Q=i}if(qe.hidden=n,Ge.toggleAttribute("data-index",n),q.hidden=n,Le.hidden=!n,Me.setAttribute("aria-current",i==="home"?"page":"false"),Oe.setAttribute("aria-current",i==="list"?"page":"false"),st.hidden=!be,At.hidden=n||!N,Qe.hidden=!(i==="thread"&&p?.status==="closed"),Je.textContent=i==="home"?d:i==="list"?"Messages":i==="new"?"New conversation":p?.subject||"Conversation",i==="home"&&(mn(),bn()),i==="list"&&(hn(),rt.textContent=C===null?"No conversations yet.":"No older conversations.",rt.hidden=T||!ee||k.length>0,Se.hidden=C===null,Ce.hidden=K===null,it.hidden=Se.hidden&&Ce.hidden),i==="thread"){let m=v.scrollHeight-v.scrollTop-v.clientHeight<48;ce.hidden=R===null,ce.disabled=P,ce.textContent=P?"Loading\u2026":"Load older messages",Ht.hidden=!te,ue.setAttribute("aria-busy",String(T)),yn()&&m&&(v.scrollTop=v.scrollHeight)}w.readOnly=E,z.disabled=E;let r=E?"Sending\u2026":"Send";z.querySelector("span").textContent=r,z.setAttribute("aria-label",r),z.title=r,q.setAttribute("aria-busy",String(E)),xn();let s=x??(T?{kind:"load",text:"Loading\u2026",error:!1}:null);V.textContent=s?.text??"",V.classList.toggle("sr",s?.kind==="load"&&!s.error),Ee.hidden=!(T&&(i==="list"&&k.length===0||i==="thread"&&h.length===0)),V.toggleAttribute("data-error",s?.error===!0)}function lt(){w.style.height="auto",w.style.height=`${Math.min(144,Math.max(24,w.scrollHeight))}px`}function Ne(){let e=_.get(fe());w.value=e?.body??"",lt()}function He(){!g||f||(i==="home"?pe:i==="list"?ot:w).focus()}function ze(){if(f||!g||i!=="thread"||!p||T||Ke||document.visibilityState==="hidden"||Date.now()<ye)return;let e=re,n=p.id;e===null||U.get(n)===e||vn(n,e,L)}async function vn(e,n,t){Ke=!0;try{let r=an(await B(()=>l.markRead({conversationId:e,messageId:n})));if(f||(U.delete(e),U.set(e,n),U.size>Hn&&U.delete(U.keys().next().value),se=0,ye=0,A(t))||re!==n)return;Ft(r.hasUnread),k=k.map(s=>s.id===e?{...s,unread:!1}:s),p?.id===e&&(p={...p,unread:!1}),b()}catch(r){if(f)return;se+=1,ye=Date.now()+Math.min(ln*2**(se-1),ft),j(r)}finally{Ke=!1,!f&&se===0&&ze()}}function Re(){ie!==void 0&&clearTimeout(ie),ie=void 0}function ge(e){Re(),!(!y||f||g||document.visibilityState==="hidden")&&(ie=setTimeout(()=>{ie=void 0,wn()},e??Math.min(_n*2**ae,ft)))}async function wn(){if(Ye){ge();return}Ye=!0;let e=L,n=G;try{let t=await _e(null);if(A(e))return;Ie(null,t,n),ae=0}catch(t){if(A(e))return;ae+=1,j(t)}finally{Ye=!1}b(),ge()}function J(){ne!==void 0&&clearTimeout(ne),ne=void 0}function Pe(){if(J(),f||!g||i==="new"||document.visibilityState==="hidden")return;let e=Math.min(ln*2**oe,ft);ne=setTimeout(()=>{ne=void 0,Gt()},e)}async function Gt(){if($e){Pe();return}$e=!0;let e=L;try{if(i==="thread"&&p){je+=1;let n=p.id,t=De(await B(()=>l.listMessages({conversationId:n})));if(A(e))return;if(Wt(t),je%Rn===0){let r=C,s=G,m=await _e(r);if(A(e))return;Ie(r,m,s)}}else if(i==="list"||i==="home"){let n=C,t=G,r=await _e(n);if(A(e))return;Ie(n,r,t)}oe=0,(x?.kind==="poll"||x?.kind==="load")&&(x=null)}catch(n){if(A(e))return;oe+=1,j(n),x?.kind!=="send"&&(x={kind:"poll",text:"Can't refresh right now. Retrying.",error:!0})}finally{$e=!1}b(),ze(),Pe()}function pt(e,n){return L+=1,J(),i=e,p=n,h=[],We="",re=null,Xe=new Set,Ae.replaceChildren(),R=null,xe=0,te=!1,T=!1,P=!1,x=null,oe=0,je=0,se=0,ye=0,Ne(),L}async function $(e,n=e==="home"?null:C){xt=e;let t=pt(e,null);n!==C&&(C=n,K=null,ee=!1,k=[]),T=!0,b(),He();let r=G;try{let s=await _e(n);if(A(t))return;Ie(n,s,r)}catch(s){if(A(t))return;j(s),x={kind:"load",text:"Couldn't load conversations. Retrying.",error:!0}}T=!1,b(),Pe()}async function he(e){let n=pt("thread",e);T=!0,b(),He();try{let t=De(await B(()=>l.listMessages({conversationId:e.id})));if(A(n))return;Wt(t)}catch(t){if(A(n))return;j(t),x={kind:"load",text:"Couldn't load messages. Retrying.",error:!0}}T=!1,b(),v.scrollTop=v.scrollHeight,ze(),Pe()}function qt(){pt("new",null),b(),He()}let Jt=()=>C===null&&ee?k.find(e=>e.status==="open"):void 0;function kn(){let e=Jt();e?he(e):qt()}async function Sn(){let e=R;if(!p||e===null||P)return;let n=p.id,t=L;P=!0,b();try{let s=De(await B(()=>l.listMessages({conversationId:n,cursor:e})));if(A(t))return;if(R===e){xe+=1;let m=gt-h.length,c=new Set(h.map(ct=>ct.id)),W=s.messages.filter(ct=>!c.has(ct.id));h=[...m>0?W.slice(-m):[],...h];let me=W.length>=m||xe>=In;te=me&&s.nextCursor!==null,R=me?null:s.nextCursor,x?.kind==="load"&&(x=null)}}catch(s){if(A(t))return;j(s),x={kind:"load",text:"Couldn't load older messages. Try again.",error:!0}}P=!1;let r=v.scrollHeight;b(),v.scrollTop+=v.scrollHeight-r,ze()}async function Qt(){if(E||f||i!=="thread"&&i!=="new")return;let e=fe(),n=i==="thread"?p:null,t=Bt(e),r=t.body.trim();if(r.length===0||r.length>Be){x={kind:"send",text:r.length===0?"Write a message first.":`Your message is too long. The limit is ${Be} characters.`,error:!0},b();return}let s=JSON.stringify([e,r]);t.attempt?.payload!==s&&(t.attempt={payload:s,requestId:Vn()});let{requestId:m}=t.attempt;E=!0,zt=e,F.replaceChildren(o("p","bubble",r),o("span","delivery","Sending\u2026")),w.value="",lt(),x=null,b(),v.scrollTop=v.scrollHeight;try{if(n){let c=rn(await B(()=>l.sendMessage({conversationId:n.id,body:r,requestId:m})));if(f)return;_.delete(e),Vt({...n,status:"open",updatedAt:c.createdAt,preview:c.body}),i==="thread"&&p?.id===n.id&&($t([c]),Ne())}else{let c=nn(await B(()=>l.createConversation({body:r,requestId:m})));if(f)return;if(_.delete(e),C!==null&&i==="new"&&(C=null,K=null,ee=!1,k=[]),Vt(c),i==="new"&&Ne(),g&&i==="new"){E=!1,he(c);return}}}catch(c){if(f)return;j(c),fe()===e&&Ne(),x={kind:"send",text:"Message not sent. Your draft is kept. Try again.",error:!0}}E=!1,b(),i==="thread"&&(v.scrollTop=v.scrollHeight)}function Cn(){let e=Bt(fe());e.body=w.value,lt(),x?.kind==="send"&&(x=null,b())}function Zt(){if(document.visibilityState==="hidden"){J(),Re();return}if(!g){ae=0,ge(0);return}T||i==="new"||(oe=0,J(),Gt())}I.addEventListener("click",()=>g?Ue():tn()),Et.addEventListener("click",()=>Ue()),qe.addEventListener("click",()=>{$(xt)}),ot.addEventListener("click",()=>qt()),pe.addEventListener("click",()=>kn()),Me.addEventListener("click",()=>{$("home")}),Oe.addEventListener("click",()=>{$("list")}),ce.addEventListener("click",()=>{Sn()}),Se.addEventListener("click",()=>{$("list",null)}),Ce.addEventListener("click",()=>{K!==null&&$("list",K)}),w.addEventListener("input",Cn),q.addEventListener("submit",e=>{e.preventDefault(),Qt()}),w.addEventListener("keydown",e=>{e.key!=="Enter"||e.isComposing||e.shiftKey||(e.preventDefault(),Qt())}),S.addEventListener("keydown",e=>{e.key==="Escape"&&Ue()});for(let e of["keydown","keyup","keypress"])de.addEventListener(e,n=>n.stopPropagation());document.addEventListener("visibilitychange",Zt);function en(){try{a.onOpenChange?.(g)}catch{}}function tn(e){if(!(f||g)){if(g=!0,L+=1,Re(),i==="thread"&&p)he(p);else if(i==="new")b(),He();else if(e==="messages"||i==="list")$("list");else{let n=Jt();n?he(n):$("home")}en()}}function Ue(){if(f||!g)return;g=!1,L+=1,J(),T=!1,P=!1;let e=ve.activeElement!==null;b(),S.inert=!0,Y?(S.dataset.state="closing",Z=setTimeout(()=>{Z=void 0,S.hidden=!0},160)):S.hidden=!0,e&&I.focus(),en(),ae=0,ge(be?0:void 0)}function An(){f||(f=!0,F.replaceChildren(),g=!1,L+=1,J(),Re(),clearTimeout(Z),kt.cancelAll(),document.removeEventListener("visibilitychange",Zt),_.clear(),U.clear(),M.remove())}return b(),(a.launcherContainer??document.body??document.documentElement).append(M),ge(0),{open:tn,close:Ue,destroy:An,controlsContainer:dt}}export{Be as MAX_BODY_LENGTH,En as MAX_REQUEST_ID_LENGTH,Tn as MAX_SUBJECT_LENGTH,ut as SupportError,$n as createSupportWidget};
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { g as SupportIdentity, l as SupportTransport } from './types-CLIkWasY.js';
|
|
2
|
+
export { C as CreateConversationInput, L as ListConversationsInput, a as ListMessagesInput, M as MarkReadInput, S as SendMessageInput, c as SupportConversation, d as SupportConversationPage, e as SupportError, f as SupportErrorCode, h as SupportMessage, i as SupportPage, j as SupportReadResult } from './types-CLIkWasY.js';
|
|
3
|
+
|
|
4
|
+
declare const REQUEST_TIMEOUT_MS = 8000;
|
|
5
|
+
declare const MAX_RESPONSE_BYTES: number;
|
|
6
|
+
declare const MAX_DISPLAY_NAME_LENGTH = 200;
|
|
7
|
+
type FetchLike = (input: string, init: RequestInit) => Promise<Response>;
|
|
8
|
+
type SupportClientOptions = {
|
|
9
|
+
/** Full endpoint URL, for example the Intray `/v1/support/chat` route. */
|
|
10
|
+
url: string;
|
|
11
|
+
/** Workspace secret. Sent only as a bearer token to `url`. */
|
|
12
|
+
key: string;
|
|
13
|
+
fetch?: FetchLike;
|
|
14
|
+
};
|
|
15
|
+
type SupportClient = {
|
|
16
|
+
/**
|
|
17
|
+
* `identity` must come from the host's own verified session on every
|
|
18
|
+
* request. Never build it from fields the browser sent.
|
|
19
|
+
*/
|
|
20
|
+
forIdentity(identity: SupportIdentity): SupportTransport;
|
|
21
|
+
};
|
|
22
|
+
declare function createSupportClient(options: SupportClientOptions): SupportClient;
|
|
23
|
+
|
|
24
|
+
export { MAX_DISPLAY_NAME_LENGTH, MAX_RESPONSE_BYTES, REQUEST_TIMEOUT_MS, type SupportClient, type SupportClientOptions, SupportIdentity, SupportTransport, createSupportClient };
|
package/dist/server.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{a as o,e as w,f as b,g as m,h as c,i as I,j as R,k as v,l as C,m as k,n as P}from"./chunk-3IC2MERJ.js";var x=8e3,T=2*1024*1024,_=200,A=300,q=new Set(["localhost","127.0.0.1","[::1]"]);function N(e){return q.has(e)||e.endsWith(".localhost")}function y(e){return new o("invalid_config",e)}function j(e){if(typeof e!="string")throw y("url must be a string");let n;try{n=new URL(e)}catch{throw y("url is not a valid absolute URL")}let s=n.protocol==="https:",a=n.protocol==="http:"&&N(n.hostname);if(!s&&!a)throw y("url must use https, or http on localhost");if(n.username!==""||n.password!=="")throw y("url must not contain credentials");if(n.hash!=="")throw y("url must not contain a fragment");return n.href}function U(e){if(typeof e!="string"||!/^[\x21-\x7e]+$/.test(e))throw y("key must be a non-empty string without whitespace");return e}function z(e){if(typeof e!="object"||e===null)throw new o("invalid_input","identity must be an object");let n=e,s=typeof n.displayName=="string"?n.displayName.trim():"";if(s.length===0||s.length>_)throw new o("invalid_input",`identity.displayName must be 1 to ${_} characters`);let a;try{let p=new URL(String(n.origin));(p.protocol==="https:"||p.protocol==="http:")&&(a=p.origin)}catch{}if(a===void 0||a!==n.origin)throw new o("invalid_input","identity.origin must be an http(s) origin such as https://app.example");return Object.freeze({userId:c(n.userId,"identity.userId"),accountId:c(n.accountId,"identity.accountId"),displayName:s,origin:a})}async function E(e){let n=Number(e.headers.get("content-length")),s=e.body;if(n>T)throw await s?.cancel().catch(()=>{}),new o("too_large","Support response is too large");if(!s)return"";let a=s.getReader(),p=[],h=0;for(;;){let{done:r,value:t}=await a.read();if(r)break;if(h+=t.byteLength,h>T)throw await a.cancel().catch(()=>{}),new o("too_large","Support response is too large");p.push(t)}let u=new Uint8Array(h),i=0;for(let r of p)u.set(r,i),i+=r.byteLength;return new TextDecoder("utf-8",{fatal:!0}).decode(u)}function L(e){try{return JSON.parse(e)}catch{throw new o("invalid_response","Support response is not JSON")}}function J(e){let n=j(e.url),s=U(e.key),a=e.fetch??((i,r)=>globalThis.fetch(i,r)),p=i=>i.split(s).join("[redacted]");async function h(i,r){let t=await a(n,{method:"POST",headers:{Authorization:`Bearer ${s}`,"Content-Type":"application/json",Accept:"application/json"},body:i,redirect:"manual",signal:r});if(t.type==="opaqueredirect"||t.status>=300&&t.status<400)throw await t.body?.cancel().catch(()=>{}),new o("redirect","Support endpoint sent a redirect");if(!t.ok){let g=`Support request failed with status ${t.status}`;try{let l=L(await E(t))?.error;typeof l=="string"&&l.length>0&&(g=p(l).slice(0,A))}catch{}throw new o("http",g,t.status)}let d=L(await E(t));if(typeof d!="object"||d===null||!("data"in d))throw new o("invalid_response","Support response has no data field");return d.data}async function u(i,r,t,d){let g=JSON.stringify({identity:i,operation:r,...t}),S=new AbortController,l,M=new Promise((f,O)=>{l=setTimeout(()=>{S.abort(),O(new o("timeout","Support request timed out"))},x)});try{let f=await Promise.race([h(g,S.signal),M]);return d(f)}catch(f){throw f instanceof o?f:S.signal.aborted?new o("timeout","Support request timed out"):new o("network","Support request failed to connect")}finally{clearTimeout(l)}}return{forIdentity(i){let r=z(i);return{async listConversations(t){return u(r,"list",{cursor:I(t?.cursor)},v)},async createConversation(t){return u(r,"create",{body:w(t.body),requestId:m(t.requestId),subject:b(t.subject)},R)},async listMessages(t){return u(r,"messages",{conversationId:c(t.conversationId,"conversationId"),cursor:I(t.cursor)},k)},async sendMessage(t){return u(r,"send",{conversationId:c(t.conversationId,"conversationId"),body:w(t.body),requestId:m(t.requestId)},C)},async markRead(t){return u(r,"read",{conversationId:c(t.conversationId,"conversationId"),messageId:c(t.messageId,"messageId")},P)}}}}}export{_ as MAX_DISPLAY_NAME_LENGTH,T as MAX_RESPONSE_BYTES,x as REQUEST_TIMEOUT_MS,o as SupportError,J as createSupportClient};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
type SupportErrorCode = "invalid_input" | "invalid_config" | "network" | "timeout" | "redirect" | "too_large" | "invalid_response" | "http";
|
|
2
|
+
declare class SupportError extends Error {
|
|
3
|
+
readonly code: SupportErrorCode;
|
|
4
|
+
/** HTTP status for `http` errors. */
|
|
5
|
+
readonly status?: number;
|
|
6
|
+
constructor(code: SupportErrorCode, message: string, status?: number);
|
|
7
|
+
/** A retry with the same `requestId` can succeed. */
|
|
8
|
+
get retryable(): boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type SupportConversation = {
|
|
12
|
+
id: string;
|
|
13
|
+
subject: string;
|
|
14
|
+
status: "open" | "closed";
|
|
15
|
+
updatedAt: number;
|
|
16
|
+
preview: string;
|
|
17
|
+
/** An operator message the customer has not acknowledged with `markRead`. */
|
|
18
|
+
unread: boolean;
|
|
19
|
+
/** Verified profile of the operator who replied last. Absent before a reply. */
|
|
20
|
+
responder?: SupportTeamMember;
|
|
21
|
+
};
|
|
22
|
+
type SupportMessage = {
|
|
23
|
+
/** Verified responder profile, supplied by the support backend. */
|
|
24
|
+
authorName?: string;
|
|
25
|
+
authorAvatarUrl?: string;
|
|
26
|
+
id: string;
|
|
27
|
+
author: "customer" | "operator";
|
|
28
|
+
body: string;
|
|
29
|
+
createdAt: number;
|
|
30
|
+
};
|
|
31
|
+
/** A support person. `avatarUrl` loads over HTTPS only. */
|
|
32
|
+
type SupportTeamMember = {
|
|
33
|
+
name: string;
|
|
34
|
+
avatarUrl?: string;
|
|
35
|
+
};
|
|
36
|
+
/** `messages` are in ascending order. `nextCursor` loads the page before them. */
|
|
37
|
+
type SupportPage = {
|
|
38
|
+
messages: SupportMessage[];
|
|
39
|
+
nextCursor: string | null;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* `conversations` are newest first. `nextCursor` loads the older page after
|
|
43
|
+
* them. `hasUnread` covers every conversation of the customer, not this page.
|
|
44
|
+
*/
|
|
45
|
+
type SupportConversationPage = {
|
|
46
|
+
conversations: SupportConversation[];
|
|
47
|
+
nextCursor: string | null;
|
|
48
|
+
hasUnread: boolean;
|
|
49
|
+
/** The application's name and logo. Shown where no person has replied yet. */
|
|
50
|
+
brand?: SupportBrand;
|
|
51
|
+
};
|
|
52
|
+
type SupportBrand = {
|
|
53
|
+
name: string;
|
|
54
|
+
logoUrl?: string;
|
|
55
|
+
};
|
|
56
|
+
type ListConversationsInput = {
|
|
57
|
+
cursor?: string;
|
|
58
|
+
};
|
|
59
|
+
/** Verified by the host backend from its own session. Never sent by a browser. */
|
|
60
|
+
type SupportIdentity = {
|
|
61
|
+
userId: string;
|
|
62
|
+
accountId: string;
|
|
63
|
+
displayName: string;
|
|
64
|
+
origin: string;
|
|
65
|
+
};
|
|
66
|
+
type CreateConversationInput = {
|
|
67
|
+
body: string;
|
|
68
|
+
requestId: string;
|
|
69
|
+
subject?: string;
|
|
70
|
+
};
|
|
71
|
+
type ListMessagesInput = {
|
|
72
|
+
conversationId: string;
|
|
73
|
+
cursor?: string;
|
|
74
|
+
};
|
|
75
|
+
type SendMessageInput = {
|
|
76
|
+
conversationId: string;
|
|
77
|
+
body: string;
|
|
78
|
+
requestId: string;
|
|
79
|
+
};
|
|
80
|
+
/** `messageId` is the newest operator message the customer has on screen. */
|
|
81
|
+
type MarkReadInput = {
|
|
82
|
+
conversationId: string;
|
|
83
|
+
messageId: string;
|
|
84
|
+
};
|
|
85
|
+
/** `hasUnread` covers every conversation of the customer, after the call. */
|
|
86
|
+
type SupportReadResult = {
|
|
87
|
+
hasUnread: boolean;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* `requestId` makes create and send idempotent. A retry of the same payload
|
|
91
|
+
* reuses the same `requestId`, and the server returns the first result.
|
|
92
|
+
*/
|
|
93
|
+
interface SupportTransport {
|
|
94
|
+
listConversations(args?: ListConversationsInput): Promise<SupportConversationPage>;
|
|
95
|
+
createConversation(input: CreateConversationInput): Promise<SupportConversation>;
|
|
96
|
+
listMessages(input: ListMessagesInput): Promise<SupportPage>;
|
|
97
|
+
sendMessage(input: SendMessageInput): Promise<SupportMessage>;
|
|
98
|
+
/**
|
|
99
|
+
* The server clears unread only when `messageId` is still the newest
|
|
100
|
+
* operator message, so a reply that races the call stays unread.
|
|
101
|
+
*/
|
|
102
|
+
markRead(input: MarkReadInput): Promise<SupportReadResult>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export { type CreateConversationInput as C, type ListConversationsInput as L, type MarkReadInput as M, type SendMessageInput as S, type ListMessagesInput as a, type SupportBrand as b, type SupportConversation as c, type SupportConversationPage as d, SupportError as e, type SupportErrorCode as f, type SupportIdentity as g, type SupportMessage as h, type SupportPage as i, type SupportReadResult as j, type SupportTeamMember as k, type SupportTransport as l };
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@intray/support",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Support chat widget and server client for Intray. No framework, no runtime dependencies.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"chat",
|
|
7
|
+
"intray",
|
|
8
|
+
"support",
|
|
9
|
+
"widget"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://intray.ai/docs",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": "Lowside Labs, Inc.",
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"type": "module",
|
|
18
|
+
"sideEffects": false,
|
|
19
|
+
"main": "./dist/index.js",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"import": "./dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./server": {
|
|
27
|
+
"types": "./dist/server.d.ts",
|
|
28
|
+
"import": "./dist/server.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"jsdom": "^29.1.1",
|
|
36
|
+
"tsup": "8.5.1",
|
|
37
|
+
"typescript": "^6.0.3",
|
|
38
|
+
"vitest": "^4.1.8"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"check-types": "tsc --noEmit",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"build": "tsup"
|
|
44
|
+
}
|
|
45
|
+
}
|