@clivly/chat-widget 0.3.0-next.7 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  Embeddable chat widget package for Clivly conversations.
4
4
 
5
+ > **Preview-quality surface.** Install with `npm install @clivly/chat-widget`.
6
+ > The end-to-end path (SDK session handler, `/widget.js` serving, operator
7
+ > inbox) is implemented and covered by integration tests; treat it as
8
+ > design-partner quality while the conversations UX settles.
9
+
10
+ > **New here?** Start with
11
+ > [Adding the chat widget](../../docs/guides/adding-the-chat-widget.md) — the
12
+ > end-to-end integration guide (widget record, session route, embed, operator
13
+ > inbox). This README is the API-level reference for the package's entrypoints.
14
+
5
15
  This package currently exposes four stable entrypoints with different intended
6
16
  use cases.
7
17
 
@@ -106,6 +106,23 @@ const WIDGET_STYLES = `
106
106
  font-size: 0.75rem;
107
107
  margin-top: 0.35rem;
108
108
  }
109
+ .clivly-chat-widget__powered {
110
+ border-top: 1px solid var(--clivly-chat-border);
111
+ color: var(--clivly-chat-muted);
112
+ font-size: 0.6875rem;
113
+ font-style: italic;
114
+ margin: 0;
115
+ padding: 0.5rem 1rem;
116
+ text-align: center;
117
+ }
118
+ .clivly-chat-widget__powered a {
119
+ color: inherit;
120
+ text-decoration: none;
121
+ }
122
+ .clivly-chat-widget__powered a:hover,
123
+ .clivly-chat-widget__powered a:focus-visible {
124
+ text-decoration: underline;
125
+ }
109
126
  .clivly-chat-widget__composer,
110
127
  .clivly-chat-widget__form {
111
128
  border-top: 1px solid var(--clivly-chat-border);
@@ -164,7 +181,7 @@ const WIDGET_STYLES = `
164
181
  text-align: center;
165
182
  }
166
183
  `;
167
- const DEFAULT_STORAGE = typeof window !== "undefined" ? window.localStorage : void 0;
184
+ const DEFAULT_STORAGE = typeof window === "undefined" ? void 0 : window.localStorage;
168
185
  function createLocalMessage(message, delivery = "sent") {
169
186
  return {
170
187
  ...message,
@@ -184,6 +201,14 @@ function formatTimestamp(value) {
184
201
  day: "numeric"
185
202
  }).format(date);
186
203
  }
204
+ /**
205
+ * Enter sends the draft. Shift+Enter still inserts a newline, and an
206
+ * in-progress IME composition (CJK) must never be sent — committing a
207
+ * composition also fires Enter, which would truncate the word being typed.
208
+ */
209
+ function isSendKey(event) {
210
+ return event.key === "Enter" && !(event.shiftKey || event.nativeEvent.isComposing);
211
+ }
187
212
  function generateId() {
188
213
  if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
189
214
  return `msg_${Math.random().toString(36).slice(2, 10)}`;
@@ -229,11 +254,9 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
229
254
  const transportRef = (0, react.useRef)(null);
230
255
  const listRef = (0, react.useRef)(null);
231
256
  const themeStyle = useThemeStyle(session);
232
- (0, react.useEffect)(() => {
233
- return () => {
234
- transportRef.current?.close();
235
- transportRef.current = null;
236
- };
257
+ (0, react.useEffect)(() => () => {
258
+ transportRef.current?.close();
259
+ transportRef.current = null;
237
260
  }, []);
238
261
  (0, react.useEffect)(() => {
239
262
  if (!listRef.current) return;
@@ -243,7 +266,7 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
243
266
  transportRef.current?.close();
244
267
  transportRef.current = null;
245
268
  setIsReady(false);
246
- if (!session || !createTransport) return;
269
+ if (!(session && createTransport)) return;
247
270
  const handleEvent = (event) => {
248
271
  switch (event.type) {
249
272
  case "conversation.snapshot":
@@ -310,8 +333,8 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
310
333
  }
311
334
  };
312
335
  const submitMessage = (event) => {
313
- event.preventDefault();
314
- if (!session || !transportRef.current) {
336
+ event?.preventDefault();
337
+ if (!(session && transportRef.current)) {
315
338
  setError("Chat is not connected yet.");
316
339
  return;
317
340
  }
@@ -393,10 +416,10 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
393
416
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: message.author ?? "Clivly" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatTimestamp(message.createdAt) })]
394
417
  }),
395
418
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: message.body }),
396
- message.delivery !== "sent" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
419
+ message.delivery === "sent" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
397
420
  className: "clivly-chat-widget__status",
398
421
  children: message.delivery === "pending" ? "Sending…" : "Failed to send"
399
- }) : null
422
+ })
400
423
  ]
401
424
  }, message.id)) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
402
425
  className: "clivly-chat-widget__empty",
@@ -419,6 +442,11 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
419
442
  onChange: (event) => {
420
443
  setMessageDraft(event.target.value);
421
444
  },
445
+ onKeyDown: (event) => {
446
+ if (!isSendKey(event)) return;
447
+ event.preventDefault();
448
+ if (messageDraft.trim() && isReady) submitMessage();
449
+ },
422
450
  placeholder: "Write your message",
423
451
  value: messageDraft
424
452
  })]
@@ -429,7 +457,7 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
429
457
  children: isReady || !createTransport ? "Conversation ready" : "Connecting…"
430
458
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
431
459
  className: "clivly-chat-widget__button",
432
- disabled: !messageDraft.trim() || !isReady,
460
+ disabled: !(messageDraft.trim() && isReady),
433
461
  type: "submit",
434
462
  children: "Send"
435
463
  })]
@@ -488,6 +516,15 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
488
516
  })]
489
517
  })
490
518
  ]
519
+ }),
520
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
521
+ className: "clivly-chat-widget__powered",
522
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
523
+ href: "https://clivly.com",
524
+ rel: "noopener",
525
+ target: "_blank",
526
+ children: "Powered by Clivly"
527
+ })
491
528
  })
492
529
  ]
493
530
  }) : null
@@ -105,6 +105,23 @@ const WIDGET_STYLES = `
105
105
  font-size: 0.75rem;
106
106
  margin-top: 0.35rem;
107
107
  }
108
+ .clivly-chat-widget__powered {
109
+ border-top: 1px solid var(--clivly-chat-border);
110
+ color: var(--clivly-chat-muted);
111
+ font-size: 0.6875rem;
112
+ font-style: italic;
113
+ margin: 0;
114
+ padding: 0.5rem 1rem;
115
+ text-align: center;
116
+ }
117
+ .clivly-chat-widget__powered a {
118
+ color: inherit;
119
+ text-decoration: none;
120
+ }
121
+ .clivly-chat-widget__powered a:hover,
122
+ .clivly-chat-widget__powered a:focus-visible {
123
+ text-decoration: underline;
124
+ }
108
125
  .clivly-chat-widget__composer,
109
126
  .clivly-chat-widget__form {
110
127
  border-top: 1px solid var(--clivly-chat-border);
@@ -163,7 +180,7 @@ const WIDGET_STYLES = `
163
180
  text-align: center;
164
181
  }
165
182
  `;
166
- const DEFAULT_STORAGE = typeof window !== "undefined" ? window.localStorage : void 0;
183
+ const DEFAULT_STORAGE = typeof window === "undefined" ? void 0 : window.localStorage;
167
184
  function createLocalMessage(message, delivery = "sent") {
168
185
  return {
169
186
  ...message,
@@ -183,6 +200,14 @@ function formatTimestamp(value) {
183
200
  day: "numeric"
184
201
  }).format(date);
185
202
  }
203
+ /**
204
+ * Enter sends the draft. Shift+Enter still inserts a newline, and an
205
+ * in-progress IME composition (CJK) must never be sent — committing a
206
+ * composition also fires Enter, which would truncate the word being typed.
207
+ */
208
+ function isSendKey(event) {
209
+ return event.key === "Enter" && !(event.shiftKey || event.nativeEvent.isComposing);
210
+ }
186
211
  function generateId() {
187
212
  if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
188
213
  return `msg_${Math.random().toString(36).slice(2, 10)}`;
@@ -228,11 +253,9 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
228
253
  const transportRef = useRef(null);
229
254
  const listRef = useRef(null);
230
255
  const themeStyle = useThemeStyle(session);
231
- useEffect(() => {
232
- return () => {
233
- transportRef.current?.close();
234
- transportRef.current = null;
235
- };
256
+ useEffect(() => () => {
257
+ transportRef.current?.close();
258
+ transportRef.current = null;
236
259
  }, []);
237
260
  useEffect(() => {
238
261
  if (!listRef.current) return;
@@ -242,7 +265,7 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
242
265
  transportRef.current?.close();
243
266
  transportRef.current = null;
244
267
  setIsReady(false);
245
- if (!session || !createTransport) return;
268
+ if (!(session && createTransport)) return;
246
269
  const handleEvent = (event) => {
247
270
  switch (event.type) {
248
271
  case "conversation.snapshot":
@@ -309,8 +332,8 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
309
332
  }
310
333
  };
311
334
  const submitMessage = (event) => {
312
- event.preventDefault();
313
- if (!session || !transportRef.current) {
335
+ event?.preventDefault();
336
+ if (!(session && transportRef.current)) {
314
337
  setError("Chat is not connected yet.");
315
338
  return;
316
339
  }
@@ -392,10 +415,10 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
392
415
  children: [/* @__PURE__ */ jsx("span", { children: message.author ?? "Clivly" }), /* @__PURE__ */ jsx("span", { children: formatTimestamp(message.createdAt) })]
393
416
  }),
394
417
  /* @__PURE__ */ jsx("div", { children: message.body }),
395
- message.delivery !== "sent" ? /* @__PURE__ */ jsx("div", {
418
+ message.delivery === "sent" ? null : /* @__PURE__ */ jsx("div", {
396
419
  className: "clivly-chat-widget__status",
397
420
  children: message.delivery === "pending" ? "Sending…" : "Failed to send"
398
- }) : null
421
+ })
399
422
  ]
400
423
  }, message.id)) : /* @__PURE__ */ jsx("p", {
401
424
  className: "clivly-chat-widget__empty",
@@ -418,6 +441,11 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
418
441
  onChange: (event) => {
419
442
  setMessageDraft(event.target.value);
420
443
  },
444
+ onKeyDown: (event) => {
445
+ if (!isSendKey(event)) return;
446
+ event.preventDefault();
447
+ if (messageDraft.trim() && isReady) submitMessage();
448
+ },
421
449
  placeholder: "Write your message",
422
450
  value: messageDraft
423
451
  })]
@@ -428,7 +456,7 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
428
456
  children: isReady || !createTransport ? "Conversation ready" : "Connecting…"
429
457
  }), /* @__PURE__ */ jsx("button", {
430
458
  className: "clivly-chat-widget__button",
431
- disabled: !messageDraft.trim() || !isReady,
459
+ disabled: !(messageDraft.trim() && isReady),
432
460
  type: "submit",
433
461
  children: "Send"
434
462
  })]
@@ -487,6 +515,15 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
487
515
  })]
488
516
  })
489
517
  ]
518
+ }),
519
+ /* @__PURE__ */ jsx("p", {
520
+ className: "clivly-chat-widget__powered",
521
+ children: /* @__PURE__ */ jsx("a", {
522
+ href: "https://clivly.com",
523
+ rel: "noopener",
524
+ target: "_blank",
525
+ children: "Powered by Clivly"
526
+ })
490
527
  })
491
528
  ]
492
529
  }) : null
package/dist/browser.cjs CHANGED
@@ -106,6 +106,23 @@ const WIDGET_STYLES = `
106
106
  font-size: 0.75rem;
107
107
  margin-top: 0.35rem;
108
108
  }
109
+ .clivly-chat-widget__powered {
110
+ border-top: 1px solid var(--clivly-chat-border);
111
+ color: var(--clivly-chat-muted);
112
+ font-size: 0.6875rem;
113
+ font-style: italic;
114
+ margin: 0;
115
+ padding: 0.5rem 1rem;
116
+ text-align: center;
117
+ }
118
+ .clivly-chat-widget__powered a {
119
+ color: inherit;
120
+ text-decoration: none;
121
+ }
122
+ .clivly-chat-widget__powered a:hover,
123
+ .clivly-chat-widget__powered a:focus-visible {
124
+ text-decoration: underline;
125
+ }
109
126
  .clivly-chat-widget__composer,
110
127
  .clivly-chat-widget__form {
111
128
  border-top: 1px solid var(--clivly-chat-border);
@@ -164,7 +181,7 @@ const WIDGET_STYLES = `
164
181
  text-align: center;
165
182
  }
166
183
  `;
167
- const DEFAULT_STORAGE = typeof window !== "undefined" ? window.localStorage : void 0;
184
+ const DEFAULT_STORAGE = typeof window === "undefined" ? void 0 : window.localStorage;
168
185
  function createLocalMessage(message, delivery = "sent") {
169
186
  return {
170
187
  ...message,
@@ -184,6 +201,14 @@ function formatTimestamp(value) {
184
201
  day: "numeric"
185
202
  }).format(date);
186
203
  }
204
+ /**
205
+ * Enter sends the draft. Shift+Enter still inserts a newline, and an
206
+ * in-progress IME composition (CJK) must never be sent — committing a
207
+ * composition also fires Enter, which would truncate the word being typed.
208
+ */
209
+ function isSendKey(event) {
210
+ return event.key === "Enter" && !(event.shiftKey || event.nativeEvent.isComposing);
211
+ }
187
212
  function generateId() {
188
213
  if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
189
214
  return `msg_${Math.random().toString(36).slice(2, 10)}`;
@@ -229,11 +254,9 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
229
254
  const transportRef = (0, react.useRef)(null);
230
255
  const listRef = (0, react.useRef)(null);
231
256
  const themeStyle = useThemeStyle(session);
232
- (0, react.useEffect)(() => {
233
- return () => {
234
- transportRef.current?.close();
235
- transportRef.current = null;
236
- };
257
+ (0, react.useEffect)(() => () => {
258
+ transportRef.current?.close();
259
+ transportRef.current = null;
237
260
  }, []);
238
261
  (0, react.useEffect)(() => {
239
262
  if (!listRef.current) return;
@@ -243,7 +266,7 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
243
266
  transportRef.current?.close();
244
267
  transportRef.current = null;
245
268
  setIsReady(false);
246
- if (!session || !createTransport) return;
269
+ if (!(session && createTransport)) return;
247
270
  const handleEvent = (event) => {
248
271
  switch (event.type) {
249
272
  case "conversation.snapshot":
@@ -310,8 +333,8 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
310
333
  }
311
334
  };
312
335
  const submitMessage = (event) => {
313
- event.preventDefault();
314
- if (!session || !transportRef.current) {
336
+ event?.preventDefault();
337
+ if (!(session && transportRef.current)) {
315
338
  setError("Chat is not connected yet.");
316
339
  return;
317
340
  }
@@ -393,10 +416,10 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
393
416
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: message.author ?? "Clivly" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatTimestamp(message.createdAt) })]
394
417
  }),
395
418
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: message.body }),
396
- message.delivery !== "sent" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
419
+ message.delivery === "sent" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
397
420
  className: "clivly-chat-widget__status",
398
421
  children: message.delivery === "pending" ? "Sending…" : "Failed to send"
399
- }) : null
422
+ })
400
423
  ]
401
424
  }, message.id)) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
402
425
  className: "clivly-chat-widget__empty",
@@ -419,6 +442,11 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
419
442
  onChange: (event) => {
420
443
  setMessageDraft(event.target.value);
421
444
  },
445
+ onKeyDown: (event) => {
446
+ if (!isSendKey(event)) return;
447
+ event.preventDefault();
448
+ if (messageDraft.trim() && isReady) submitMessage();
449
+ },
422
450
  placeholder: "Write your message",
423
451
  value: messageDraft
424
452
  })]
@@ -429,7 +457,7 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
429
457
  children: isReady || !createTransport ? "Conversation ready" : "Connecting…"
430
458
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
431
459
  className: "clivly-chat-widget__button",
432
- disabled: !messageDraft.trim() || !isReady,
460
+ disabled: !(messageDraft.trim() && isReady),
433
461
  type: "submit",
434
462
  children: "Send"
435
463
  })]
@@ -488,6 +516,15 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
488
516
  })]
489
517
  })
490
518
  ]
519
+ }),
520
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
521
+ className: "clivly-chat-widget__powered",
522
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
523
+ href: "https://clivly.com",
524
+ rel: "noopener",
525
+ target: "_blank",
526
+ children: "Powered by Clivly"
527
+ })
491
528
  })
492
529
  ]
493
530
  }) : null
package/dist/browser.mjs CHANGED
@@ -105,6 +105,23 @@ const WIDGET_STYLES = `
105
105
  font-size: 0.75rem;
106
106
  margin-top: 0.35rem;
107
107
  }
108
+ .clivly-chat-widget__powered {
109
+ border-top: 1px solid var(--clivly-chat-border);
110
+ color: var(--clivly-chat-muted);
111
+ font-size: 0.6875rem;
112
+ font-style: italic;
113
+ margin: 0;
114
+ padding: 0.5rem 1rem;
115
+ text-align: center;
116
+ }
117
+ .clivly-chat-widget__powered a {
118
+ color: inherit;
119
+ text-decoration: none;
120
+ }
121
+ .clivly-chat-widget__powered a:hover,
122
+ .clivly-chat-widget__powered a:focus-visible {
123
+ text-decoration: underline;
124
+ }
108
125
  .clivly-chat-widget__composer,
109
126
  .clivly-chat-widget__form {
110
127
  border-top: 1px solid var(--clivly-chat-border);
@@ -163,7 +180,7 @@ const WIDGET_STYLES = `
163
180
  text-align: center;
164
181
  }
165
182
  `;
166
- const DEFAULT_STORAGE = typeof window !== "undefined" ? window.localStorage : void 0;
183
+ const DEFAULT_STORAGE = typeof window === "undefined" ? void 0 : window.localStorage;
167
184
  function createLocalMessage(message, delivery = "sent") {
168
185
  return {
169
186
  ...message,
@@ -183,6 +200,14 @@ function formatTimestamp(value) {
183
200
  day: "numeric"
184
201
  }).format(date);
185
202
  }
203
+ /**
204
+ * Enter sends the draft. Shift+Enter still inserts a newline, and an
205
+ * in-progress IME composition (CJK) must never be sent — committing a
206
+ * composition also fires Enter, which would truncate the word being typed.
207
+ */
208
+ function isSendKey(event) {
209
+ return event.key === "Enter" && !(event.shiftKey || event.nativeEvent.isComposing);
210
+ }
186
211
  function generateId() {
187
212
  if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
188
213
  return `msg_${Math.random().toString(36).slice(2, 10)}`;
@@ -228,11 +253,9 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
228
253
  const transportRef = useRef(null);
229
254
  const listRef = useRef(null);
230
255
  const themeStyle = useThemeStyle(session);
231
- useEffect(() => {
232
- return () => {
233
- transportRef.current?.close();
234
- transportRef.current = null;
235
- };
256
+ useEffect(() => () => {
257
+ transportRef.current?.close();
258
+ transportRef.current = null;
236
259
  }, []);
237
260
  useEffect(() => {
238
261
  if (!listRef.current) return;
@@ -242,7 +265,7 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
242
265
  transportRef.current?.close();
243
266
  transportRef.current = null;
244
267
  setIsReady(false);
245
- if (!session || !createTransport) return;
268
+ if (!(session && createTransport)) return;
246
269
  const handleEvent = (event) => {
247
270
  switch (event.type) {
248
271
  case "conversation.snapshot":
@@ -309,8 +332,8 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
309
332
  }
310
333
  };
311
334
  const submitMessage = (event) => {
312
- event.preventDefault();
313
- if (!session || !transportRef.current) {
335
+ event?.preventDefault();
336
+ if (!(session && transportRef.current)) {
314
337
  setError("Chat is not connected yet.");
315
338
  return;
316
339
  }
@@ -392,10 +415,10 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
392
415
  children: [/* @__PURE__ */ jsx("span", { children: message.author ?? "Clivly" }), /* @__PURE__ */ jsx("span", { children: formatTimestamp(message.createdAt) })]
393
416
  }),
394
417
  /* @__PURE__ */ jsx("div", { children: message.body }),
395
- message.delivery !== "sent" ? /* @__PURE__ */ jsx("div", {
418
+ message.delivery === "sent" ? null : /* @__PURE__ */ jsx("div", {
396
419
  className: "clivly-chat-widget__status",
397
420
  children: message.delivery === "pending" ? "Sending…" : "Failed to send"
398
- }) : null
421
+ })
399
422
  ]
400
423
  }, message.id)) : /* @__PURE__ */ jsx("p", {
401
424
  className: "clivly-chat-widget__empty",
@@ -418,6 +441,11 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
418
441
  onChange: (event) => {
419
442
  setMessageDraft(event.target.value);
420
443
  },
444
+ onKeyDown: (event) => {
445
+ if (!isSendKey(event)) return;
446
+ event.preventDefault();
447
+ if (messageDraft.trim() && isReady) submitMessage();
448
+ },
421
449
  placeholder: "Write your message",
422
450
  value: messageDraft
423
451
  })]
@@ -428,7 +456,7 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
428
456
  children: isReady || !createTransport ? "Conversation ready" : "Connecting…"
429
457
  }), /* @__PURE__ */ jsx("button", {
430
458
  className: "clivly-chat-widget__button",
431
- disabled: !messageDraft.trim() || !isReady,
459
+ disabled: !(messageDraft.trim() && isReady),
432
460
  type: "submit",
433
461
  children: "Send"
434
462
  })]
@@ -487,6 +515,15 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
487
515
  })]
488
516
  })
489
517
  ]
518
+ }),
519
+ /* @__PURE__ */ jsx("p", {
520
+ className: "clivly-chat-widget__powered",
521
+ children: /* @__PURE__ */ jsx("a", {
522
+ href: "https://clivly.com",
523
+ rel: "noopener",
524
+ target: "_blank",
525
+ children: "Powered by Clivly"
526
+ })
490
527
  })
491
528
  ]
492
529
  }) : null
package/dist/index.cjs CHANGED
@@ -106,6 +106,23 @@ const WIDGET_STYLES = `
106
106
  font-size: 0.75rem;
107
107
  margin-top: 0.35rem;
108
108
  }
109
+ .clivly-chat-widget__powered {
110
+ border-top: 1px solid var(--clivly-chat-border);
111
+ color: var(--clivly-chat-muted);
112
+ font-size: 0.6875rem;
113
+ font-style: italic;
114
+ margin: 0;
115
+ padding: 0.5rem 1rem;
116
+ text-align: center;
117
+ }
118
+ .clivly-chat-widget__powered a {
119
+ color: inherit;
120
+ text-decoration: none;
121
+ }
122
+ .clivly-chat-widget__powered a:hover,
123
+ .clivly-chat-widget__powered a:focus-visible {
124
+ text-decoration: underline;
125
+ }
109
126
  .clivly-chat-widget__composer,
110
127
  .clivly-chat-widget__form {
111
128
  border-top: 1px solid var(--clivly-chat-border);
@@ -164,7 +181,7 @@ const WIDGET_STYLES = `
164
181
  text-align: center;
165
182
  }
166
183
  `;
167
- const DEFAULT_STORAGE = typeof window !== "undefined" ? window.localStorage : void 0;
184
+ const DEFAULT_STORAGE = typeof window === "undefined" ? void 0 : window.localStorage;
168
185
  function createLocalMessage(message, delivery = "sent") {
169
186
  return {
170
187
  ...message,
@@ -184,6 +201,14 @@ function formatTimestamp(value) {
184
201
  day: "numeric"
185
202
  }).format(date);
186
203
  }
204
+ /**
205
+ * Enter sends the draft. Shift+Enter still inserts a newline, and an
206
+ * in-progress IME composition (CJK) must never be sent — committing a
207
+ * composition also fires Enter, which would truncate the word being typed.
208
+ */
209
+ function isSendKey(event) {
210
+ return event.key === "Enter" && !(event.shiftKey || event.nativeEvent.isComposing);
211
+ }
187
212
  function generateId() {
188
213
  if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
189
214
  return `msg_${Math.random().toString(36).slice(2, 10)}`;
@@ -229,11 +254,9 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
229
254
  const transportRef = (0, react.useRef)(null);
230
255
  const listRef = (0, react.useRef)(null);
231
256
  const themeStyle = useThemeStyle(session);
232
- (0, react.useEffect)(() => {
233
- return () => {
234
- transportRef.current?.close();
235
- transportRef.current = null;
236
- };
257
+ (0, react.useEffect)(() => () => {
258
+ transportRef.current?.close();
259
+ transportRef.current = null;
237
260
  }, []);
238
261
  (0, react.useEffect)(() => {
239
262
  if (!listRef.current) return;
@@ -243,7 +266,7 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
243
266
  transportRef.current?.close();
244
267
  transportRef.current = null;
245
268
  setIsReady(false);
246
- if (!session || !createTransport) return;
269
+ if (!(session && createTransport)) return;
247
270
  const handleEvent = (event) => {
248
271
  switch (event.type) {
249
272
  case "conversation.snapshot":
@@ -310,8 +333,8 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
310
333
  }
311
334
  };
312
335
  const submitMessage = (event) => {
313
- event.preventDefault();
314
- if (!session || !transportRef.current) {
336
+ event?.preventDefault();
337
+ if (!(session && transportRef.current)) {
315
338
  setError("Chat is not connected yet.");
316
339
  return;
317
340
  }
@@ -393,10 +416,10 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
393
416
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: message.author ?? "Clivly" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatTimestamp(message.createdAt) })]
394
417
  }),
395
418
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: message.body }),
396
- message.delivery !== "sent" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
419
+ message.delivery === "sent" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
397
420
  className: "clivly-chat-widget__status",
398
421
  children: message.delivery === "pending" ? "Sending…" : "Failed to send"
399
- }) : null
422
+ })
400
423
  ]
401
424
  }, message.id)) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
402
425
  className: "clivly-chat-widget__empty",
@@ -419,6 +442,11 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
419
442
  onChange: (event) => {
420
443
  setMessageDraft(event.target.value);
421
444
  },
445
+ onKeyDown: (event) => {
446
+ if (!isSendKey(event)) return;
447
+ event.preventDefault();
448
+ if (messageDraft.trim() && isReady) submitMessage();
449
+ },
422
450
  placeholder: "Write your message",
423
451
  value: messageDraft
424
452
  })]
@@ -429,7 +457,7 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
429
457
  children: isReady || !createTransport ? "Conversation ready" : "Connecting…"
430
458
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
431
459
  className: "clivly-chat-widget__button",
432
- disabled: !messageDraft.trim() || !isReady,
460
+ disabled: !(messageDraft.trim() && isReady),
433
461
  type: "submit",
434
462
  children: "Send"
435
463
  })]
@@ -488,6 +516,15 @@ function ChatWidget({ className, createTransport, getSession, initiallyOpen = fa
488
516
  })]
489
517
  })
490
518
  ]
519
+ }),
520
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
521
+ className: "clivly-chat-widget__powered",
522
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
523
+ href: "https://clivly.com",
524
+ rel: "noopener",
525
+ target: "_blank",
526
+ children: "Powered by Clivly"
527
+ })
491
528
  })
492
529
  ]
493
530
  }) : null