@paramms/chat-widget 1.0.45 → 1.0.47

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.
@@ -85,6 +85,7 @@ export interface ChatListOptions {
85
85
  /** Brand colour hex — default '#6c5ce7' */
86
86
  accent?: string;
87
87
  theme?: 'auto' | 'light' | 'dark';
88
+ webfont?: boolean;
88
89
  /** i18n overrides */
89
90
  i18n?: {
90
91
  title?: string;
package/dist/chatlist.js CHANGED
@@ -3,7 +3,7 @@ const ee = `
3
3
  .ocl { ${Q("ocl")}
4
4
  display:flex; flex-direction:column; height:100%; background:var(--ocl-bg);
5
5
  font-family:var(--ocl-fb); color:var(--ocl-ink); overflow:hidden; }
6
- @media (prefers-color-scheme: dark) { .ocl:not([data-theme="light"]) { ${H("ocl")} } }
6
+ @media (prefers-color-scheme: dark) { .ocl[data-theme="auto"] { ${H("ocl")} } }
7
7
  .ocl[data-theme="dark"] { ${H("ocl")} }
8
8
  .ocl-head { display:flex; align-items:center; padding:16px 16px 10px; background:var(--ocl-bg); }
9
9
  .ocl-title { flex:1; display:flex; align-items:center; color:var(--ocl-accent); }
@@ -32,7 +32,7 @@ const ee = `
32
32
  .ocl-status.waiting { background:var(--ocl-tint); color:var(--ocl-accent); border:1px solid var(--ocl-accent); }
33
33
  .ocl-status.done { background:#eee; color:#777; }
34
34
  .ocl[data-theme="dark"] .ocl-status.done { background:#39325e; color:#b3abd6; }
35
- @media (prefers-color-scheme: dark) { .ocl:not([data-theme="light"]) .ocl-status.done { background:#39325e; color:#b3abd6; } }
35
+ @media (prefers-color-scheme: dark) { .ocl[data-theme="auto"] .ocl-status.done { background:#39325e; color:#b3abd6; } }
36
36
  .ocl-spinner { padding:24px; text-align:center; color:var(--ocl-mut); font-size:13px; }
37
37
  .ocl-compose { border:none; background:var(--ocl-accent); color:var(--ocl-onaccent); width:32px; height:32px; border-radius:50%; font-size:18px; line-height:1; cursor:pointer; box-shadow:var(--ocl-shadow); }
38
38
  .ocl-compose:hover { filter:brightness(1.08); }
@@ -87,40 +87,44 @@ function ae(o) {
87
87
  const e = document.createElement("style");
88
88
  e.id = "ocl-styles", e.textContent = ee, document.head.append(e);
89
89
  }
90
- const h = t("div", "ocl");
91
- h.style.setProperty("--ocl-accent", G), o.theme && o.theme !== "auto" && (h.dataset.theme = o.theme), o.reserveCloseSpace && h.classList.add("ocl-has-close");
90
+ if (o.webfont !== !1 && typeof document < "u" && !document.getElementById("ocw-webfont")) {
91
+ const e = document.createElement("link");
92
+ e.id = "ocw-webfont", e.rel = "stylesheet", e.href = "https://fonts.googleapis.com/css2?family=Baloo+2:wght@500;600;700&family=Nunito:wght@400;500;600;700&display=swap", document.head.append(e);
93
+ }
94
+ const u = t("div", "ocl");
95
+ u.style.setProperty("--ocl-accent", G), u.dataset.theme = o.theme ?? "light", o.reserveCloseSpace && u.classList.add("ocl-has-close");
92
96
  const z = t("div", "ocl-head"), C = t("span", "ocl-title");
93
97
  if (C.setAttribute("role", "img"), C.setAttribute("aria-label", f.title ?? "Messages"), C.innerHTML = ne, z.append(C), o.onNewChat) {
94
98
  const e = t("button", "ocl-compose", "✎");
95
99
  e.title = f.newChat ?? "New conversation", e.addEventListener("click", () => o.onNewChat()), z.append(e);
96
100
  }
97
- const R = t("div", "ocl-search-wrap"), v = t("input", "ocl-search");
98
- v.placeholder = f.search ?? "🔍 Search", v.type = "search", R.append(v);
101
+ const R = t("div", "ocl-search-wrap"), m = t("input", "ocl-search");
102
+ m.placeholder = f.search ?? "🔍 Search", m.type = "search", R.append(m);
99
103
  const s = t("div", "ocl-body");
100
- s.append(t("div", "ocl-spinner", "Loading…")), h.append(z, R, s), o.el.replaceChildren(h);
101
- const T = (e) => {
102
- h.classList.toggle("ocl-compact", e > 0 && e < 400);
104
+ s.append(t("div", "ocl-spinner", "Loading…")), u.append(z, R, s), o.el.replaceChildren(u);
105
+ const E = (e) => {
106
+ u.classList.toggle("ocl-compact", e > 0 && e < 400);
103
107
  };
104
- T(h.clientWidth);
105
- let m = null;
106
- if (typeof ResizeObserver < "u" && (m = new ResizeObserver((e) => {
108
+ E(u.clientWidth);
109
+ let v = null;
110
+ if (typeof ResizeObserver < "u" && (v = new ResizeObserver((e) => {
107
111
  var r;
108
- return T(((r = e[0]) == null ? void 0 : r.contentRect.width) ?? h.clientWidth);
109
- }), m.observe(h)), !o.profileId && !o.tenantId) throw new Error("[relay chatlist] provide profileId or tenantId");
110
- let M;
111
- const N = `ocl_seen_${o.profileId ?? `t_${o.tenantId}`}_${(o.userId ?? c).slice(-8)}`;
112
+ return E(((r = e[0]) == null ? void 0 : r.contentRect.width) ?? u.clientWidth);
113
+ }), v.observe(u)), !o.profileId && !o.tenantId) throw new Error("[relay chatlist] provide profileId or tenantId");
114
+ let N;
115
+ const T = `ocl_seen_${o.profileId ?? `t_${o.tenantId}`}_${(o.userId ?? c).slice(-8)}`;
112
116
  let b = {};
113
117
  try {
114
- b = JSON.parse(localStorage.getItem(N) ?? "{}");
118
+ b = JSON.parse(localStorage.getItem(T) ?? "{}");
115
119
  } catch {
116
120
  }
117
121
  const J = () => {
118
122
  try {
119
- localStorage.setItem(N, JSON.stringify(b));
123
+ localStorage.setItem(T, JSON.stringify(b));
120
124
  } catch {
121
125
  }
122
126
  };
123
- let E = [], g = !1;
127
+ let M = [], g = !1;
124
128
  const K = async () => {
125
129
  const e = o.profileId ? `profileId=${encodeURIComponent(o.profileId)}${o.scope === "tenant" ? "&scope=tenant" : ""}` : `tenantId=${encodeURIComponent(o.tenantId)}`, r = `${p}/conversations/mine?${e}`, l = { authorization: `Bearer ${c}` };
126
130
  let d;
@@ -131,7 +135,7 @@ function ae(o) {
131
135
  }
132
136
  if (!d.ok) return [];
133
137
  const i = await d.json();
134
- return i.defaultProfileId && (M = i.defaultProfileId), (i.conversations ?? []).sort((u, n) => n.updatedAt - u.updatedAt);
138
+ return i.defaultProfileId && (N = i.defaultProfileId), (i.conversations ?? []).sort((h, n) => n.updatedAt - h.updatedAt);
135
139
  }, O = (e, r) => {
136
140
  if (g) return;
137
141
  const l = r ? e.filter(
@@ -147,18 +151,18 @@ function ae(o) {
147
151
  s.append(n);
148
152
  return;
149
153
  }
150
- const d = (n) => (n.lastSeq ?? 0) > (b[n.id] ?? 0), i = l.filter(d), u = l.filter((n) => !d(n));
154
+ const d = (n) => (n.lastSeq ?? 0) > (b[n.id] ?? 0), i = l.filter(d), h = l.filter((n) => !d(n));
151
155
  if (i.length) {
152
156
  s.append(t("div", "ocl-section", `${f.unread ?? "Unread"} (${i.length})`));
153
157
  for (const n of i) s.append(A(n, d(n)));
154
158
  }
155
- if (u.length) {
159
+ if (h.length) {
156
160
  s.append(t("div", "ocl-section", i.length ? f.all ?? "All conversations" : ""));
157
- for (const n of u) s.append(A(n, !1));
161
+ for (const n of h) s.append(A(n, !1));
158
162
  }
159
163
  }, U = (e) => e.subjectTitle ?? (e.kind === "direct" ? e.peerId ?? "Direct message" : "General enquiry"), A = (e, r) => {
160
164
  var P;
161
- const l = U(e), d = (((P = l.match(new RegExp("\\p{L}", "u"))) == null ? void 0 : P[0]) ?? l.trim()[0] ?? "?").toUpperCase(), i = e.lastSeq ?? 0, u = r ? Math.max(1, i - (b[e.id] ?? 0)) : 0, n = t("button", `ocl-row${r ? " unread" : ""}`), S = t("div", "ocl-av", d);
165
+ const l = U(e), d = (((P = l.match(new RegExp("\\p{L}", "u"))) == null ? void 0 : P[0]) ?? l.trim()[0] ?? "?").toUpperCase(), i = e.lastSeq ?? 0, h = r ? Math.max(1, i - (b[e.id] ?? 0)) : 0, n = t("button", `ocl-row${r ? " unread" : ""}`), S = t("div", "ocl-av", d);
162
166
  n.append(S);
163
167
  const $ = t("div", "ocl-info");
164
168
  $.append(t("div", "ocl-name", l));
@@ -172,13 +176,13 @@ function ae(o) {
172
176
  const k = t("div", "ocl-right");
173
177
  k.append(t("div", "ocl-time", te(e.updatedAt)));
174
178
  const L = oe(e.state);
175
- return L && k.append(t("div", `ocl-status ${L.cls}`, L.label)), u > 0 && k.append(t("div", "ocl-badge", String(u > 99 ? "99+" : u))), n.append(k), n.addEventListener("click", () => {
179
+ return L && k.append(t("div", `ocl-status ${L.cls}`, L.label)), h > 0 && k.append(t("div", "ocl-badge", String(h > 99 ? "99+" : h))), n.append(k), n.addEventListener("click", () => {
176
180
  var q;
177
181
  i > 0 && (b[e.id] = i, J()), n.classList.remove("unread"), (q = k.querySelector(".ocl-badge")) == null || q.remove(), o.onSelect(e);
178
182
  }), n;
179
183
  }, I = () => {
180
184
  g || K().then((e) => {
181
- g || (E = e, O(e, v.value.trim().toLowerCase()));
185
+ g || (M = e, O(e, m.value.trim().toLowerCase()));
182
186
  }).catch((e) => {
183
187
  if (g) return;
184
188
  console.error(`[chat-widget] failed to load conversations from ${p}/conversations/mine — check the apiUrl/CORS config.`, e);
@@ -190,13 +194,13 @@ function ae(o) {
190
194
  }), r.append(l), s.replaceChildren(r);
191
195
  });
192
196
  };
193
- v.addEventListener("input", () => O(E, v.value.trim().toLowerCase())), I();
194
- let a = null, w, y, _ = 0, W = !1;
195
- const j = () => {
197
+ m.addEventListener("input", () => O(M, m.value.trim().toLowerCase())), I();
198
+ let a = null, w, y, _ = 0, B = !1;
199
+ const W = () => {
196
200
  y || (y = setTimeout(() => {
197
201
  y = void 0, I();
198
202
  }, 300));
199
- }, B = () => {
203
+ }, j = () => {
200
204
  if (!g) {
201
205
  try {
202
206
  a = new WebSocket(x);
@@ -205,10 +209,10 @@ function ae(o) {
205
209
  return;
206
210
  }
207
211
  a.binaryType = "arraybuffer", a.onopen = () => {
208
- _ = 0, a.send(F({ type: "auth", token: c })), a.send(F({ type: "subscribe_inbox" })), W && j(), W = !0;
212
+ _ = 0, a.send(F({ type: "auth", token: c })), a.send(F({ type: "subscribe_inbox" })), B && W(), B = !0;
209
213
  }, a.onmessage = (e) => {
210
214
  const r = Z(new Uint8Array(e.data));
211
- r && r.type === "inbox_event" && j();
215
+ r && r.type === "inbox_event" && W();
212
216
  }, a.onclose = () => {
213
217
  a = null, D();
214
218
  }, a.onerror = () => {
@@ -222,13 +226,13 @@ function ae(o) {
222
226
  if (g || w) return;
223
227
  const e = Math.min(15e3, 500 * 2 ** _++) + Math.random() * 250;
224
228
  w = setTimeout(() => {
225
- w = void 0, B();
229
+ w = void 0, j();
226
230
  }, e);
227
231
  };
228
- return B(), {
232
+ return j(), {
229
233
  refresh: I,
230
234
  close() {
231
- g = !0, w && clearTimeout(w), y && clearTimeout(y), m == null || m.disconnect(), m = null;
235
+ g = !0, w && clearTimeout(w), y && clearTimeout(y), v == null || v.disconnect(), v = null;
232
236
  try {
233
237
  a == null || a.close();
234
238
  } catch {
@@ -236,7 +240,7 @@ function ae(o) {
236
240
  a = null, o.el.replaceChildren();
237
241
  },
238
242
  defaultProfileId() {
239
- return o.profileId ?? M;
243
+ return o.profileId ?? N;
240
244
  }
241
245
  };
242
246
  }
@@ -1 +1 @@
1
- {"version":3,"file":"chatlist.js","sources":["../src/chatlist.styles.ts","../src/chatlist.ts"],"sourcesContent":["// Injected stylesheet for the chat list (`.ocl`). Extracted from chatlist.ts.\n// Tokens come from theme-tokens.ts (single source of truth).\nimport { lightTokens, darkTokens } from './theme-tokens.js'\n\nexport const CSS = `\n.ocl { ${lightTokens('ocl')}\n display:flex; flex-direction:column; height:100%; background:var(--ocl-bg);\n font-family:var(--ocl-fb); color:var(--ocl-ink); overflow:hidden; }\n@media (prefers-color-scheme: dark) { .ocl:not([data-theme=\"light\"]) { ${darkTokens('ocl')} } }\n.ocl[data-theme=\"dark\"] { ${darkTokens('ocl')} }\n.ocl-head { display:flex; align-items:center; padding:16px 16px 10px; background:var(--ocl-bg); }\n.ocl-title { flex:1; display:flex; align-items:center; color:var(--ocl-accent); }\n.ocl-title svg { width:24px; height:24px; display:block; }\n.ocl-retry { margin-top:14px; border:none; background:var(--ocl-accent); color:var(--ocl-onaccent); border-radius:20px; padding:8px 20px; font:inherit; font-size:13px; font-weight:600; cursor:pointer; }\n.ocl-search-wrap { padding:4px 12px 8px; background:var(--ocl-bg); }\n.ocl-search { width:100%; box-sizing:border-box; border:none; background:var(--ocl-tint); border-radius:20px; padding:9px 14px; font:inherit; font-size:14px; outline:none; }\n.ocl-body { flex:1; overflow-y:auto; }\n.ocl-section { padding:10px 18px 4px; font-family:var(--ocl-fh); font-size:11px; font-weight:600; color:var(--ocl-mut); text-transform:uppercase; letter-spacing:.5px; background:transparent; }\n.ocl-empty { padding:40px 20px; text-align:center; color:var(--ocl-mut); font-size:14px; }\n.ocl-row { display:flex; align-items:center; gap:12px; padding:10px 12px; margin:6px 12px; background:var(--ocl-tint); border:none; width:calc(100% - 24px); box-sizing:border-box; border-radius:16px; text-align:left; cursor:pointer; transition:background .12s; }\n.ocl-row:hover { background:var(--ocl-rowhover); }\n.ocl-row.unread { background:var(--ocl-rowhover); }\n.ocl-av { width:40px; height:40px; border-radius:50%; background:var(--ocl-accent); color:var(--ocl-onaccent); font-family:var(--ocl-fh); font-size:15px; font-weight:600; display:flex; align-items:center; justify-content:center; flex:none; }\n.ocl-info { flex:1; min-width:0; }\n.ocl-name { font-family:var(--ocl-fh); font-size:14px; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; margin-bottom:2px; }\n.ocl-row.unread .ocl-name { font-weight:700; }\n.ocl-preview { font-size:11.5px; color:var(--ocl-mut); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }\n.ocl-row.unread .ocl-preview { color:var(--ocl-ink); }\n.ocl-right { display:flex; flex-direction:column; align-items:flex-end; gap:4px; flex:none; }\n.ocl-time { font-size:10px; color:var(--ocl-mut); }\n.ocl-row.unread .ocl-time { color:var(--ocl-accent); font-weight:600; }\n.ocl-badge { background:var(--ocl-accent); color:var(--ocl-onaccent); border-radius:999px; font-size:11px; font-weight:700; min-width:20px; height:20px; padding:0 5px; display:flex; align-items:center; justify-content:center; }\n.ocl-status { font-size:10px; font-weight:600; padding:2px 8px; border-radius:20px; white-space:nowrap; }\n.ocl-status.open { background:#dff3e6; color:#2f8a52; }\n.ocl-status.waiting { background:var(--ocl-tint); color:var(--ocl-accent); border:1px solid var(--ocl-accent); }\n.ocl-status.done { background:#eee; color:#777; }\n.ocl[data-theme=\"dark\"] .ocl-status.done { background:#39325e; color:#b3abd6; }\n@media (prefers-color-scheme: dark) { .ocl:not([data-theme=\"light\"]) .ocl-status.done { background:#39325e; color:#b3abd6; } }\n.ocl-spinner { padding:24px; text-align:center; color:var(--ocl-mut); font-size:13px; }\n.ocl-compose { border:none; background:var(--ocl-accent); color:var(--ocl-onaccent); width:32px; height:32px; border-radius:50%; font-size:18px; line-height:1; cursor:pointer; box-shadow:var(--ocl-shadow); }\n.ocl-compose:hover { filter:brightness(1.08); }\n/* When the HOST overlays a ✕ (ChatApp's onClose / embed's inbox stack), it is\n absolutely positioned at top-right and used to land straight on top of the ✎\n compose button. Reserve the space instead of stacking them. */\n.ocl-has-close .ocl-head { padding-right:52px; }\n.ocl-start { margin-top:12px; border:none; background:var(--ocl-accent); color:var(--ocl-onaccent); border-radius:20px; padding:8px 18px; font:inherit; font-size:13px; font-weight:600; cursor:pointer; }\n/* Compact sizing keyed on the LIST'S OWN width (ResizeObserver toggles\n * .ocl-compact below 400px) — covers phones and narrow desktop embeds alike.\n * Search must be ≥16px in compact or iOS zooms the page on focus. */\n.ocl-compact .ocl-row { padding:13px 14px; }\n.ocl-compact .ocl-search { font-size:16px; }\n.ocl-compact .ocl-compose { width:36px; height:36px; }\n.ocl-row:focus-visible, .ocl button:focus-visible, .ocl input:focus-visible { outline:2px solid var(--ocl-accent); outline-offset:2px; border-radius:12px; }\n`\n","/**\n * chatlist.ts — standalone chat list widget.\n *\n * Completely separate from mount() / the chat widget.\n * Shows all conversations for a given userId / guest on a profile.\n * Tapping a row fires onSelect(entry) — the caller decides what to do\n * (navigate to a new page, open a ChatWidget inline, etc.)\n *\n * Usage (vanilla):\n * import { mountChatList } from '@paramms/chat-widget/chatlist'\n * const handle = mountChatList({\n * el: document.getElementById('chat-list'),\n * url: 'https://api.relay.paramms.com', // ONE url, any scheme\n * profileId: 'p_usedcars',\n * userId: currentUser.id, // optional — uses localStorage UID if omitted\n * onSelect: (entry) => {\n * window.location.href = `/listings/${entry.subjectId}#chat`\n * },\n * })\n * handle.refresh() // manually re-fetch the list\n * handle.close() // unmount and clean up\n */\n\nimport { resolveRelayUrls } from './history.js'\nimport { CSS } from './chatlist.styles.js'\nimport { persistentUid } from './uid.js'\nimport { encodeFrame, decodeFrame } from './protocol/codec.js'\n\nexport interface ChatListEntry {\n id: string\n /** Chatroom this conversation belongs to. With `scope: 'tenant'` this can\n * differ from the profileId the list was mounted with — open the chat\n * against THIS profileId. */\n profileId?: string\n /** 'support' (default) or 'direct' (user↔user DM). */\n kind?: string\n /** For direct conversations: the other participant's user id. */\n peerId?: string\n subjectId?: string\n subjectTitle?: string\n /** One-line detail — e.g. \"45,000 km · Auto\" */\n subjectMeta?: string\n /** URL of the listing/item page — stored automatically when the widget first opens */\n subjectUrl?: string\n state: string\n updatedAt: number\n lastSeq?: number\n lastMessage?: string\n}\n\nexport interface ChatListOptions {\n /** Mount target element */\n el: HTMLElement\n /** Relay URL — ONE url, any scheme (https recommended). The WebSocket URL\n * and REST base are derived automatically. */\n url: string\n /** HTTP(S) base for REST — only when REST is on a different origin.\n * @deprecated pass a single `url`; kept for back-compat. */\n apiUrl?: string\n /** Profile ID to scope conversations to */\n profileId?: string\n /** Tenant-level identification — list EVERY conversation this user has\n * with the business, across ALL of its chatrooms, without naming one\n * (e.g. a platform running a marketplace chatroom AND a general-support\n * chatroom). Provide `profileId` OR `tenantId` (profileId wins if both;\n * it also fixes where \"new conversation\" opens). With only `tenantId`,\n * the compose target is the server-reported defaultProfileId (the\n * tenant's oldest chatroom). */\n tenantId?: string\n /** A signed identity token (ES256 JWT) — the production identity tier for\n * chatrooms with signed identity enabled. Wins over `userId`. */\n token?: string\n /** Your logged-in user's stable ID. Omit for anonymous (uses localStorage UID) */\n userId?: string\n /** Which conversations to list (default 'profile'):\n * 'profile' — only this chatroom's threads.\n * 'tenant' — every conversation this user has with the chatroom's owning\n * business, across ALL of its chatrooms (a real chat-app inbox). Rows\n * carry `profileId` so each opens against the right chatroom. */\n scope?: 'profile' | 'tenant'\n /** Called when the user taps a conversation row */\n onSelect: (entry: ChatListEntry) => void\n /** When provided, the list shows a ✎ compose button in the header (and a\n * \"Start a conversation\" button in the empty state) that calls this —\n * wire it to open a fresh/general thread. Without it a user with no\n * conversations yet has nothing to tap. */\n onNewChat?: () => void\n /** Set when the CALLER draws its own close (✕) control overlaying the list's\n * top-right corner — reserves header space so it doesn't sit on top of the\n * ✎ compose button. */\n reserveCloseSpace?: boolean\n /** Brand colour hex — default '#6c5ce7' */\n accent?: string\n theme?: 'auto' | 'light' | 'dark'\n /** i18n overrides */\n i18n?: {\n title?: string // default 'Messages'\n search?: string // default '🔍 Search'\n empty?: string // default 'No conversations yet.'\n unread?: string // default 'Unread'\n all?: string // default 'All conversations'\n error?: string // default 'Could not load conversations.'\n retry?: string // default 'Retry'\n close?: string // default 'Close' (aria-label for the ✕ control)\n newChat?: string // default 'New conversation' / 'Start a conversation'\n }\n}\n\nexport interface ChatListHandle {\n /** Re-fetch and re-render the list */\n refresh(): void\n /** Unmount and clean up */\n close(): void\n /** Where \"new conversation\" should open: the configured profileId, else the\n * server-reported tenant default (oldest chatroom). Undefined until the\n * first successful fetch when only tenantId was configured. */\n defaultProfileId(): string | undefined\n}\n\n/** Map a conversation state to a status chip (label + style class). Returns null\n * for states with no meaningful badge. */\nfunction statusChip(state: string): { label: string; cls: string } | null {\n switch (state) {\n case 'open': return { label: 'Open', cls: 'open' }\n case 'awaiting_staff':return { label: 'Waiting on you', cls: 'waiting' }\n case 'resolved': return { label: 'Resolved', cls: 'done' }\n case 'closed': return { label: 'Closed', cls: 'done' }\n default: return null\n }\n}\n\nfunction timeAgo(ts: number): string {\n const s = Math.floor((Date.now() - ts) / 1000)\n if (s < 60) return 'just now'\n if (s < 3600) return `${Math.floor(s / 60)}m`\n if (s < 86400) return `${Math.floor(s / 3600)}h`\n const days = Math.floor(s / 86400)\n if (days <= 7) return `${days}d`\n // Beyond a week, a date scans better than \"43d\" (what Channel.io/Intercom do).\n try { return new Date(ts).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) }\n catch { return `${days}d` }\n}\n\n// Inline, dependency-free chat-bubble glyph. `currentColor` picks up the\n// header's accent tint. No external font/emoji so it renders identically\n// across platforms (the old '💬'/text wordmark varied per-OS).\nconst CHAT_ICON_SVG =\n '<svg viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">' +\n '<path d=\"M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z\" ' +\n 'fill=\"currentColor\" fill-opacity=\"0.12\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>'\n\nfunction el(tag: string, cls?: string, text?: string): HTMLElement {\n const e = document.createElement(tag)\n if (cls) e.className = cls\n if (text !== undefined) e.textContent = text\n return e\n}\n\n// CSS lives in chatlist.styles.ts (single source of truth for .ocl styling).\n\n/** Mount a standalone chat list widget. */\nexport function mountChatList(opts: ChatListOptions): ChatListHandle {\n const token = opts.token ?? opts.userId ?? persistentUid()\n const { httpBase, wsUrl } = resolveRelayUrls(opts.url, opts.apiUrl)\n const i18n = opts.i18n ?? {}\n const accent = opts.accent ?? '#6c5ce7'\n\n // Inject the shared stylesheet ONCE, and keep it accent-FREE. The accent was\n // previously baked into this shared <style> (CSS.replace(/#f5713c/g, accent)),\n // which meant the FIRST list mounted on a page won the accent for EVERY list\n // after it (the style tag already existed, so a second list's colour was\n // ignored) — real interference when a project runs more than one widget. The\n // accent now lives in a per-instance CSS variable set on the root element\n // below, so each list keeps its own colour and the shared sheet stays static\n // (also friendlier to HTTP caching).\n if (!document.getElementById('ocl-styles')) {\n const s = document.createElement('style'); s.id = 'ocl-styles'\n s.textContent = CSS\n document.head.append(s)\n }\n\n // Build DOM\n const root = el('div', 'ocl')\n root.style.setProperty('--ocl-accent', accent) // per-instance accent\n if (opts.theme && opts.theme !== 'auto') root.dataset.theme = opts.theme\n if (opts.reserveCloseSpace) root.classList.add('ocl-has-close')\n const head = el('div', 'ocl-head')\n // Header shows a chat icon (accent-tinted) rather than a \"Messages\" wordmark.\n // The i18n title is still exposed as the accessible label for screen readers.\n const titleEl = el('span', 'ocl-title')\n titleEl.setAttribute('role', 'img')\n titleEl.setAttribute('aria-label', i18n.title ?? 'Messages')\n titleEl.innerHTML = CHAT_ICON_SVG\n head.append(titleEl)\n if (opts.onNewChat) {\n const compose = el('button', 'ocl-compose', '✎') as HTMLButtonElement\n compose.title = i18n.newChat ?? 'New conversation'\n compose.addEventListener('click', () => opts.onNewChat!())\n head.append(compose)\n }\n\n const searchWrap = el('div', 'ocl-search-wrap')\n const searchIn = el('input', 'ocl-search') as HTMLInputElement\n searchIn.placeholder = i18n.search ?? '🔍 Search'; searchIn.type = 'search'\n searchWrap.append(searchIn)\n\n const body = el('div', 'ocl-body')\n body.append(el('div', 'ocl-spinner', 'Loading…'))\n root.append(head, searchWrap, body)\n opts.el.replaceChildren(root)\n\n // Container-driven compact sizing (see the CSS note). Guarded for\n // jsdom/old runtimes without ResizeObserver — they keep desktop sizing.\n const applyCompact = (w: number): void => { root.classList.toggle('ocl-compact', w > 0 && w < 400) }\n applyCompact(root.clientWidth)\n let compactObserver: ResizeObserver | null = null\n if (typeof ResizeObserver !== 'undefined') {\n compactObserver = new ResizeObserver((entries) => applyCompact(entries[0]?.contentRect.width ?? root.clientWidth))\n compactObserver.observe(root)\n }\n\n // Track seen seqs for unread counts (persisted in localStorage)\n // Key unread tracking by the STABLE id (userId beats token here: a signed\n // JWT changes every mint, which would reset unread counts on each load).\n if (!opts.profileId && !opts.tenantId) throw new Error('[relay chatlist] provide profileId or tenantId')\n let serverDefaultProfileId: string | undefined\n const seenKey = `ocl_seen_${opts.profileId ?? `t_${opts.tenantId}`}_${(opts.userId ?? token).slice(-8)}`\n let seenSeq: Record<string, number> = {}\n try { seenSeq = JSON.parse(localStorage.getItem(seenKey) ?? '{}') } catch {}\n\n const saveSeenSeq = () => {\n try { localStorage.setItem(seenKey, JSON.stringify(seenSeq)) } catch {}\n }\n\n let allEntries: ChatListEntry[] = []\n let destroyed = false\n\n // Fetch conversations from server\n const fetchEntries = async (): Promise<ChatListEntry[]> => {\n const who = opts.profileId\n ? `profileId=${encodeURIComponent(opts.profileId)}${opts.scope === 'tenant' ? '&scope=tenant' : ''}`\n : `tenantId=${encodeURIComponent(opts.tenantId!)}` // tenant-level is inherently tenant-scoped\n const url = `${httpBase}/conversations/mine?${who}`\n const headers = { authorization: `Bearer ${token}` }\n // A GET is safe to repeat. A single transient network reject — the socket\n // still warming up right after a reload, a relay that blipped — used to\n // dead-end straight to \"Could not load conversations.\" Retry once so a blip\n // self-heals; an HTTP error (4xx/5xx) is NOT a network failure and returns\n // an empty list rather than retrying or erroring.\n let res: Response\n try {\n res = await fetch(url, { headers })\n } catch {\n res = await fetch(url, { headers }) // one immediate retry\n }\n if (!res.ok) return []\n const data = await res.json() as { conversations?: ChatListEntry[]; defaultProfileId?: string }\n if (data.defaultProfileId) serverDefaultProfileId = data.defaultProfileId\n return (data.conversations ?? []).sort((a, b) => b.updatedAt - a.updatedAt)\n }\n\n // Render rows from entries, optionally filtered by search query\n const renderRows = (entries: ChatListEntry[], query: string) => {\n if (destroyed) return\n const filtered = query\n ? entries.filter(e =>\n rowName(e).toLowerCase().includes(query) ||\n (e.lastMessage ?? '').toLowerCase().includes(query)\n )\n : entries\n\n body.replaceChildren()\n\n if (!filtered.length) {\n const empty = el('div', 'ocl-empty', query ? 'No results.' : (i18n.empty ?? 'No conversations yet.'))\n if (!query && opts.onNewChat) {\n empty.append(el('br'))\n const start = el('button', 'ocl-start', i18n.newChat ?? 'Start a conversation') as HTMLButtonElement\n start.addEventListener('click', () => opts.onNewChat!())\n empty.append(start)\n }\n body.append(empty)\n return\n }\n\n const isUnread = (e: ChatListEntry) =>\n (e.lastSeq ?? 0) > (seenSeq[e.id] ?? 0)\n\n const unread = filtered.filter(isUnread)\n const read = filtered.filter(e => !isUnread(e))\n\n if (unread.length) {\n body.append(el('div', 'ocl-section', `${i18n.unread ?? 'Unread'} (${unread.length})`))\n for (const e of unread) body.append(buildRow(e, isUnread(e)))\n }\n if (read.length) {\n body.append(el('div', 'ocl-section', unread.length ? (i18n.all ?? 'All conversations') : ''))\n for (const e of read) body.append(buildRow(e, false))\n }\n }\n\n const rowName = (entry: ChatListEntry): string =>\n entry.subjectTitle ?? (entry.kind === 'direct' ? (entry.peerId ?? 'Direct message') : 'General enquiry')\n\n const buildRow = (entry: ChatListEntry, unread: boolean): HTMLElement => {\n const name = rowName(entry)\n // Prefer the first LETTER (any script), not a leading digit/symbol — a\n // listing titled \"2018 Kia K7\" should show \"K\", not \"2\", and Korean/other\n // scripts pick their first character too.\n const initial = (name.match(/\\p{L}/u)?.[0] ?? name.trim()[0] ?? '?').toUpperCase()\n const lastSeq = entry.lastSeq ?? 0\n const unreadCount = unread ? Math.max(1, lastSeq - (seenSeq[entry.id] ?? 0)) : 0\n\n const row = el('button', `ocl-row${unread ? ' unread' : ''}`) as HTMLButtonElement\n\n // Avatar\n const av = el('div', 'ocl-av', initial)\n row.append(av)\n\n // Info\n const info = el('div', 'ocl-info')\n info.append(el('div', 'ocl-name', name))\n const stateMap: Record<string, string> = {\n open: 'Open', awaiting_staff: 'Waiting for reply…',\n resolved: 'Resolved ✓', closed: 'Closed',\n }\n info.append(el('div', 'ocl-preview', entry.lastMessage ?? stateMap[entry.state] ?? entry.state))\n row.append(info)\n\n // Right: timestamp, status chip, unread badge\n const right = el('div', 'ocl-right')\n right.append(el('div', 'ocl-time', timeAgo(entry.updatedAt)))\n const chip = statusChip(entry.state)\n if (chip) right.append(el('div', `ocl-status ${chip.cls}`, chip.label))\n if (unreadCount > 0) {\n right.append(el('div', 'ocl-badge', String(unreadCount > 99 ? '99+' : unreadCount)))\n }\n row.append(right)\n\n row.addEventListener('click', () => {\n // Mark as read\n if (lastSeq > 0) { seenSeq[entry.id] = lastSeq; saveSeenSeq() }\n row.classList.remove('unread')\n right.querySelector('.ocl-badge')?.remove()\n opts.onSelect(entry)\n })\n\n return row\n }\n\n const refresh = () => {\n if (destroyed) return\n fetchEntries().then(entries => {\n if (destroyed) return\n allEntries = entries\n renderRows(entries, searchIn.value.trim().toLowerCase())\n }).catch((e) => {\n if (destroyed) return\n console.error(`[chat-widget] failed to load conversations from ${httpBase}/conversations/mine — check the apiUrl/CORS config.`, e)\n const errBox = el('div', 'ocl-empty', i18n.error ?? 'Could not load conversations.')\n errBox.append(el('br'))\n const retry = el('button', 'ocl-retry', i18n.retry ?? 'Retry') as HTMLButtonElement\n retry.addEventListener('click', () => {\n body.replaceChildren(el('div', 'ocl-spinner', 'Loading…'))\n refresh()\n })\n errBox.append(retry)\n body.replaceChildren(errBox)\n })\n }\n\n searchIn.addEventListener('input', () => renderRows(allEntries, searchIn.value.trim().toLowerCase()))\n\n // Initial fetch\n refresh()\n\n // ── Live inbox: subscribe over WS so the list updates the instant any of the\n // guest's conversations changes, instead of only on the periodic poll. The\n // server streams `inbox_event` to the guest's OWN inbox (keyed by guestId).\n // We coalesce bursts and re-fetch (the fetch already sorts/dedupes); the poll\n // stays as a backstop for a dropped socket. ────────────────────────────────\n let sock: WebSocket | null = null\n let reconnectTimer: ReturnType<typeof setTimeout> | undefined\n let refreshTimer: ReturnType<typeof setTimeout> | undefined\n let attempt = 0\n let connectedBefore = false\n const debouncedRefresh = () => {\n if (refreshTimer) return\n refreshTimer = setTimeout(() => { refreshTimer = undefined; refresh() }, 300)\n }\n const connectInbox = () => {\n if (destroyed) return\n try { sock = new WebSocket(wsUrl) } catch { scheduleReconnect(); return }\n sock.binaryType = 'arraybuffer'\n sock.onopen = () => {\n attempt = 0\n sock!.send(encodeFrame({ type: 'auth', token }))\n sock!.send(encodeFrame({ type: 'subscribe_inbox' }))\n // On a RECONNECT (not the first connect — the initial mount already\n // fetched), catch up on anything that changed while the socket was down.\n if (connectedBefore) debouncedRefresh()\n connectedBefore = true\n }\n sock.onmessage = (ev) => {\n const frame = decodeFrame(new Uint8Array(ev.data as ArrayBuffer))\n // Any inbox change for this guest → refresh the list. `new` (a freshly\n // created thread) and `update` (new message / state) both apply.\n if (frame && frame.type === 'inbox_event') debouncedRefresh()\n }\n sock.onclose = () => { sock = null; scheduleReconnect() }\n sock.onerror = () => { try { sock?.close() } catch { /* noop */ } }\n }\n const scheduleReconnect = () => {\n if (destroyed || reconnectTimer) return\n const delay = Math.min(15_000, 500 * 2 ** attempt++) + Math.random() * 250\n reconnectTimer = setTimeout(() => { reconnectTimer = undefined; connectInbox() }, delay)\n }\n connectInbox()\n\n return {\n refresh,\n close() {\n destroyed = true\n if (reconnectTimer) clearTimeout(reconnectTimer)\n if (refreshTimer) clearTimeout(refreshTimer)\n compactObserver?.disconnect()\n compactObserver = null\n try { sock?.close() } catch { /* noop */ }\n sock = null\n opts.el.replaceChildren()\n },\n defaultProfileId() { return opts.profileId ?? serverDefaultProfileId },\n }\n}\n"],"names":["CSS","lightTokens","darkTokens","statusChip","state","timeAgo","ts","s","days","CHAT_ICON_SVG","el","tag","cls","text","e","mountChatList","opts","token","persistentUid","httpBase","wsUrl","resolveRelayUrls","i18n","accent","root","head","titleEl","compose","searchWrap","searchIn","body","applyCompact","w","compactObserver","entries","_a","serverDefaultProfileId","seenKey","seenSeq","saveSeenSeq","allEntries","destroyed","fetchEntries","who","url","headers","res","data","a","b","renderRows","query","filtered","rowName","empty","start","isUnread","unread","read","buildRow","entry","name","initial","lastSeq","unreadCount","row","av","info","stateMap","right","chip","refresh","errBox","retry","sock","reconnectTimer","refreshTimer","attempt","connectedBefore","debouncedRefresh","connectInbox","scheduleReconnect","encodeFrame","ev","frame","decodeFrame","delay"],"mappings":"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"}
1
+ {"version":3,"file":"chatlist.js","sources":["../src/chatlist.styles.ts","../src/chatlist.ts"],"sourcesContent":["// Injected stylesheet for the chat list (`.ocl`). Extracted from chatlist.ts.\n// Tokens come from theme-tokens.ts (single source of truth).\nimport { lightTokens, darkTokens } from './theme-tokens.js'\n\nexport const CSS = `\n.ocl { ${lightTokens('ocl')}\n display:flex; flex-direction:column; height:100%; background:var(--ocl-bg);\n font-family:var(--ocl-fb); color:var(--ocl-ink); overflow:hidden; }\n@media (prefers-color-scheme: dark) { .ocl[data-theme=\"auto\"] { ${darkTokens('ocl')} } }\n.ocl[data-theme=\"dark\"] { ${darkTokens('ocl')} }\n.ocl-head { display:flex; align-items:center; padding:16px 16px 10px; background:var(--ocl-bg); }\n.ocl-title { flex:1; display:flex; align-items:center; color:var(--ocl-accent); }\n.ocl-title svg { width:24px; height:24px; display:block; }\n.ocl-retry { margin-top:14px; border:none; background:var(--ocl-accent); color:var(--ocl-onaccent); border-radius:20px; padding:8px 20px; font:inherit; font-size:13px; font-weight:600; cursor:pointer; }\n.ocl-search-wrap { padding:4px 12px 8px; background:var(--ocl-bg); }\n.ocl-search { width:100%; box-sizing:border-box; border:none; background:var(--ocl-tint); border-radius:20px; padding:9px 14px; font:inherit; font-size:14px; outline:none; }\n.ocl-body { flex:1; overflow-y:auto; }\n.ocl-section { padding:10px 18px 4px; font-family:var(--ocl-fh); font-size:11px; font-weight:600; color:var(--ocl-mut); text-transform:uppercase; letter-spacing:.5px; background:transparent; }\n.ocl-empty { padding:40px 20px; text-align:center; color:var(--ocl-mut); font-size:14px; }\n.ocl-row { display:flex; align-items:center; gap:12px; padding:10px 12px; margin:6px 12px; background:var(--ocl-tint); border:none; width:calc(100% - 24px); box-sizing:border-box; border-radius:16px; text-align:left; cursor:pointer; transition:background .12s; }\n.ocl-row:hover { background:var(--ocl-rowhover); }\n.ocl-row.unread { background:var(--ocl-rowhover); }\n.ocl-av { width:40px; height:40px; border-radius:50%; background:var(--ocl-accent); color:var(--ocl-onaccent); font-family:var(--ocl-fh); font-size:15px; font-weight:600; display:flex; align-items:center; justify-content:center; flex:none; }\n.ocl-info { flex:1; min-width:0; }\n.ocl-name { font-family:var(--ocl-fh); font-size:14px; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; margin-bottom:2px; }\n.ocl-row.unread .ocl-name { font-weight:700; }\n.ocl-preview { font-size:11.5px; color:var(--ocl-mut); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }\n.ocl-row.unread .ocl-preview { color:var(--ocl-ink); }\n.ocl-right { display:flex; flex-direction:column; align-items:flex-end; gap:4px; flex:none; }\n.ocl-time { font-size:10px; color:var(--ocl-mut); }\n.ocl-row.unread .ocl-time { color:var(--ocl-accent); font-weight:600; }\n.ocl-badge { background:var(--ocl-accent); color:var(--ocl-onaccent); border-radius:999px; font-size:11px; font-weight:700; min-width:20px; height:20px; padding:0 5px; display:flex; align-items:center; justify-content:center; }\n.ocl-status { font-size:10px; font-weight:600; padding:2px 8px; border-radius:20px; white-space:nowrap; }\n.ocl-status.open { background:#dff3e6; color:#2f8a52; }\n.ocl-status.waiting { background:var(--ocl-tint); color:var(--ocl-accent); border:1px solid var(--ocl-accent); }\n.ocl-status.done { background:#eee; color:#777; }\n.ocl[data-theme=\"dark\"] .ocl-status.done { background:#39325e; color:#b3abd6; }\n@media (prefers-color-scheme: dark) { .ocl[data-theme=\"auto\"] .ocl-status.done { background:#39325e; color:#b3abd6; } }\n.ocl-spinner { padding:24px; text-align:center; color:var(--ocl-mut); font-size:13px; }\n.ocl-compose { border:none; background:var(--ocl-accent); color:var(--ocl-onaccent); width:32px; height:32px; border-radius:50%; font-size:18px; line-height:1; cursor:pointer; box-shadow:var(--ocl-shadow); }\n.ocl-compose:hover { filter:brightness(1.08); }\n/* When the HOST overlays a ✕ (ChatApp's onClose / embed's inbox stack), it is\n absolutely positioned at top-right and used to land straight on top of the ✎\n compose button. Reserve the space instead of stacking them. */\n.ocl-has-close .ocl-head { padding-right:52px; }\n.ocl-start { margin-top:12px; border:none; background:var(--ocl-accent); color:var(--ocl-onaccent); border-radius:20px; padding:8px 18px; font:inherit; font-size:13px; font-weight:600; cursor:pointer; }\n/* Compact sizing keyed on the LIST'S OWN width (ResizeObserver toggles\n * .ocl-compact below 400px) — covers phones and narrow desktop embeds alike.\n * Search must be ≥16px in compact or iOS zooms the page on focus. */\n.ocl-compact .ocl-row { padding:13px 14px; }\n.ocl-compact .ocl-search { font-size:16px; }\n.ocl-compact .ocl-compose { width:36px; height:36px; }\n.ocl-row:focus-visible, .ocl button:focus-visible, .ocl input:focus-visible { outline:2px solid var(--ocl-accent); outline-offset:2px; border-radius:12px; }\n`\n","/**\n * chatlist.ts — standalone chat list widget.\n *\n * Completely separate from mount() / the chat widget.\n * Shows all conversations for a given userId / guest on a profile.\n * Tapping a row fires onSelect(entry) — the caller decides what to do\n * (navigate to a new page, open a ChatWidget inline, etc.)\n *\n * Usage (vanilla):\n * import { mountChatList } from '@paramms/chat-widget/chatlist'\n * const handle = mountChatList({\n * el: document.getElementById('chat-list'),\n * url: 'https://api.relay.paramms.com', // ONE url, any scheme\n * profileId: 'p_usedcars',\n * userId: currentUser.id, // optional — uses localStorage UID if omitted\n * onSelect: (entry) => {\n * window.location.href = `/listings/${entry.subjectId}#chat`\n * },\n * })\n * handle.refresh() // manually re-fetch the list\n * handle.close() // unmount and clean up\n */\n\nimport { resolveRelayUrls } from './history.js'\nimport { CSS } from './chatlist.styles.js'\nimport { persistentUid } from './uid.js'\nimport { encodeFrame, decodeFrame } from './protocol/codec.js'\n\nexport interface ChatListEntry {\n id: string\n /** Chatroom this conversation belongs to. With `scope: 'tenant'` this can\n * differ from the profileId the list was mounted with — open the chat\n * against THIS profileId. */\n profileId?: string\n /** 'support' (default) or 'direct' (user↔user DM). */\n kind?: string\n /** For direct conversations: the other participant's user id. */\n peerId?: string\n subjectId?: string\n subjectTitle?: string\n /** One-line detail — e.g. \"45,000 km · Auto\" */\n subjectMeta?: string\n /** URL of the listing/item page — stored automatically when the widget first opens */\n subjectUrl?: string\n state: string\n updatedAt: number\n lastSeq?: number\n lastMessage?: string\n}\n\nexport interface ChatListOptions {\n /** Mount target element */\n el: HTMLElement\n /** Relay URL — ONE url, any scheme (https recommended). The WebSocket URL\n * and REST base are derived automatically. */\n url: string\n /** HTTP(S) base for REST — only when REST is on a different origin.\n * @deprecated pass a single `url`; kept for back-compat. */\n apiUrl?: string\n /** Profile ID to scope conversations to */\n profileId?: string\n /** Tenant-level identification — list EVERY conversation this user has\n * with the business, across ALL of its chatrooms, without naming one\n * (e.g. a platform running a marketplace chatroom AND a general-support\n * chatroom). Provide `profileId` OR `tenantId` (profileId wins if both;\n * it also fixes where \"new conversation\" opens). With only `tenantId`,\n * the compose target is the server-reported defaultProfileId (the\n * tenant's oldest chatroom). */\n tenantId?: string\n /** A signed identity token (ES256 JWT) — the production identity tier for\n * chatrooms with signed identity enabled. Wins over `userId`. */\n token?: string\n /** Your logged-in user's stable ID. Omit for anonymous (uses localStorage UID) */\n userId?: string\n /** Which conversations to list (default 'profile'):\n * 'profile' — only this chatroom's threads.\n * 'tenant' — every conversation this user has with the chatroom's owning\n * business, across ALL of its chatrooms (a real chat-app inbox). Rows\n * carry `profileId` so each opens against the right chatroom. */\n scope?: 'profile' | 'tenant'\n /** Called when the user taps a conversation row */\n onSelect: (entry: ChatListEntry) => void\n /** When provided, the list shows a ✎ compose button in the header (and a\n * \"Start a conversation\" button in the empty state) that calls this —\n * wire it to open a fresh/general thread. Without it a user with no\n * conversations yet has nothing to tap. */\n onNewChat?: () => void\n /** Set when the CALLER draws its own close (✕) control overlaying the list's\n * top-right corner — reserves header space so it doesn't sit on top of the\n * ✎ compose button. */\n reserveCloseSpace?: boolean\n /** Brand colour hex — default '#6c5ce7' */\n accent?: string\n theme?: 'auto' | 'light' | 'dark'\n webfont?: boolean\n /** i18n overrides */\n i18n?: {\n title?: string // default 'Messages'\n search?: string // default '🔍 Search'\n empty?: string // default 'No conversations yet.'\n unread?: string // default 'Unread'\n all?: string // default 'All conversations'\n error?: string // default 'Could not load conversations.'\n retry?: string // default 'Retry'\n close?: string // default 'Close' (aria-label for the ✕ control)\n newChat?: string // default 'New conversation' / 'Start a conversation'\n }\n}\n\nexport interface ChatListHandle {\n /** Re-fetch and re-render the list */\n refresh(): void\n /** Unmount and clean up */\n close(): void\n /** Where \"new conversation\" should open: the configured profileId, else the\n * server-reported tenant default (oldest chatroom). Undefined until the\n * first successful fetch when only tenantId was configured. */\n defaultProfileId(): string | undefined\n}\n\n/** Map a conversation state to a status chip (label + style class). Returns null\n * for states with no meaningful badge. */\nfunction statusChip(state: string): { label: string; cls: string } | null {\n switch (state) {\n case 'open': return { label: 'Open', cls: 'open' }\n case 'awaiting_staff':return { label: 'Waiting on you', cls: 'waiting' }\n case 'resolved': return { label: 'Resolved', cls: 'done' }\n case 'closed': return { label: 'Closed', cls: 'done' }\n default: return null\n }\n}\n\nfunction timeAgo(ts: number): string {\n const s = Math.floor((Date.now() - ts) / 1000)\n if (s < 60) return 'just now'\n if (s < 3600) return `${Math.floor(s / 60)}m`\n if (s < 86400) return `${Math.floor(s / 3600)}h`\n const days = Math.floor(s / 86400)\n if (days <= 7) return `${days}d`\n // Beyond a week, a date scans better than \"43d\" (what Channel.io/Intercom do).\n try { return new Date(ts).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) }\n catch { return `${days}d` }\n}\n\n// Inline, dependency-free chat-bubble glyph. `currentColor` picks up the\n// header's accent tint. No external font/emoji so it renders identically\n// across platforms (the old '💬'/text wordmark varied per-OS).\nconst CHAT_ICON_SVG =\n '<svg viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">' +\n '<path d=\"M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z\" ' +\n 'fill=\"currentColor\" fill-opacity=\"0.12\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>'\n\nfunction el(tag: string, cls?: string, text?: string): HTMLElement {\n const e = document.createElement(tag)\n if (cls) e.className = cls\n if (text !== undefined) e.textContent = text\n return e\n}\n\n// CSS lives in chatlist.styles.ts (single source of truth for .ocl styling).\n\n/** Mount a standalone chat list widget. */\nexport function mountChatList(opts: ChatListOptions): ChatListHandle {\n const token = opts.token ?? opts.userId ?? persistentUid()\n const { httpBase, wsUrl } = resolveRelayUrls(opts.url, opts.apiUrl)\n const i18n = opts.i18n ?? {}\n const accent = opts.accent ?? '#6c5ce7'\n\n // Inject the shared stylesheet ONCE, and keep it accent-FREE. The accent was\n // previously baked into this shared <style> (CSS.replace(/#f5713c/g, accent)),\n // which meant the FIRST list mounted on a page won the accent for EVERY list\n // after it (the style tag already existed, so a second list's colour was\n // ignored) — real interference when a project runs more than one widget. The\n // accent now lives in a per-instance CSS variable set on the root element\n // below, so each list keeps its own colour and the shared sheet stays static\n // (also friendlier to HTTP caching).\n if (!document.getElementById('ocl-styles')) {\n const s = document.createElement('style'); s.id = 'ocl-styles'\n s.textContent = CSS\n document.head.append(s)\n }\n // Brand webfonts (shared id with the chatroom, injected once). Opt out with webfont:false.\n if (opts.webfont !== false && typeof document !== 'undefined' && !document.getElementById('ocw-webfont')) {\n const l = document.createElement('link')\n l.id = 'ocw-webfont'; l.rel = 'stylesheet'\n l.href = 'https://fonts.googleapis.com/css2?family=Baloo+2:wght@500;600;700&family=Nunito:wght@400;500;600;700&display=swap'\n document.head.append(l)\n }\n\n // Build DOM\n const root = el('div', 'ocl')\n root.style.setProperty('--ocl-accent', accent) // per-instance accent\n // Colour scheme is opt-in, never OS-auto-detected by default (see renderer.ts):\n // unset → 'light'; 'auto' follows the OS; 'dark'/'light' force it.\n root.dataset.theme = opts.theme ?? 'light'\n if (opts.reserveCloseSpace) root.classList.add('ocl-has-close')\n const head = el('div', 'ocl-head')\n // Header shows a chat icon (accent-tinted) rather than a \"Messages\" wordmark.\n // The i18n title is still exposed as the accessible label for screen readers.\n const titleEl = el('span', 'ocl-title')\n titleEl.setAttribute('role', 'img')\n titleEl.setAttribute('aria-label', i18n.title ?? 'Messages')\n titleEl.innerHTML = CHAT_ICON_SVG\n head.append(titleEl)\n if (opts.onNewChat) {\n const compose = el('button', 'ocl-compose', '✎') as HTMLButtonElement\n compose.title = i18n.newChat ?? 'New conversation'\n compose.addEventListener('click', () => opts.onNewChat!())\n head.append(compose)\n }\n\n const searchWrap = el('div', 'ocl-search-wrap')\n const searchIn = el('input', 'ocl-search') as HTMLInputElement\n searchIn.placeholder = i18n.search ?? '🔍 Search'; searchIn.type = 'search'\n searchWrap.append(searchIn)\n\n const body = el('div', 'ocl-body')\n body.append(el('div', 'ocl-spinner', 'Loading…'))\n root.append(head, searchWrap, body)\n opts.el.replaceChildren(root)\n\n // Container-driven compact sizing (see the CSS note). Guarded for\n // jsdom/old runtimes without ResizeObserver — they keep desktop sizing.\n const applyCompact = (w: number): void => { root.classList.toggle('ocl-compact', w > 0 && w < 400) }\n applyCompact(root.clientWidth)\n let compactObserver: ResizeObserver | null = null\n if (typeof ResizeObserver !== 'undefined') {\n compactObserver = new ResizeObserver((entries) => applyCompact(entries[0]?.contentRect.width ?? root.clientWidth))\n compactObserver.observe(root)\n }\n\n // Track seen seqs for unread counts (persisted in localStorage)\n // Key unread tracking by the STABLE id (userId beats token here: a signed\n // JWT changes every mint, which would reset unread counts on each load).\n if (!opts.profileId && !opts.tenantId) throw new Error('[relay chatlist] provide profileId or tenantId')\n let serverDefaultProfileId: string | undefined\n const seenKey = `ocl_seen_${opts.profileId ?? `t_${opts.tenantId}`}_${(opts.userId ?? token).slice(-8)}`\n let seenSeq: Record<string, number> = {}\n try { seenSeq = JSON.parse(localStorage.getItem(seenKey) ?? '{}') } catch {}\n\n const saveSeenSeq = () => {\n try { localStorage.setItem(seenKey, JSON.stringify(seenSeq)) } catch {}\n }\n\n let allEntries: ChatListEntry[] = []\n let destroyed = false\n\n // Fetch conversations from server\n const fetchEntries = async (): Promise<ChatListEntry[]> => {\n const who = opts.profileId\n ? `profileId=${encodeURIComponent(opts.profileId)}${opts.scope === 'tenant' ? '&scope=tenant' : ''}`\n : `tenantId=${encodeURIComponent(opts.tenantId!)}` // tenant-level is inherently tenant-scoped\n const url = `${httpBase}/conversations/mine?${who}`\n const headers = { authorization: `Bearer ${token}` }\n // A GET is safe to repeat. A single transient network reject — the socket\n // still warming up right after a reload, a relay that blipped — used to\n // dead-end straight to \"Could not load conversations.\" Retry once so a blip\n // self-heals; an HTTP error (4xx/5xx) is NOT a network failure and returns\n // an empty list rather than retrying or erroring.\n let res: Response\n try {\n res = await fetch(url, { headers })\n } catch {\n res = await fetch(url, { headers }) // one immediate retry\n }\n if (!res.ok) return []\n const data = await res.json() as { conversations?: ChatListEntry[]; defaultProfileId?: string }\n if (data.defaultProfileId) serverDefaultProfileId = data.defaultProfileId\n return (data.conversations ?? []).sort((a, b) => b.updatedAt - a.updatedAt)\n }\n\n // Render rows from entries, optionally filtered by search query\n const renderRows = (entries: ChatListEntry[], query: string) => {\n if (destroyed) return\n const filtered = query\n ? entries.filter(e =>\n rowName(e).toLowerCase().includes(query) ||\n (e.lastMessage ?? '').toLowerCase().includes(query)\n )\n : entries\n\n body.replaceChildren()\n\n if (!filtered.length) {\n const empty = el('div', 'ocl-empty', query ? 'No results.' : (i18n.empty ?? 'No conversations yet.'))\n if (!query && opts.onNewChat) {\n empty.append(el('br'))\n const start = el('button', 'ocl-start', i18n.newChat ?? 'Start a conversation') as HTMLButtonElement\n start.addEventListener('click', () => opts.onNewChat!())\n empty.append(start)\n }\n body.append(empty)\n return\n }\n\n const isUnread = (e: ChatListEntry) =>\n (e.lastSeq ?? 0) > (seenSeq[e.id] ?? 0)\n\n const unread = filtered.filter(isUnread)\n const read = filtered.filter(e => !isUnread(e))\n\n if (unread.length) {\n body.append(el('div', 'ocl-section', `${i18n.unread ?? 'Unread'} (${unread.length})`))\n for (const e of unread) body.append(buildRow(e, isUnread(e)))\n }\n if (read.length) {\n body.append(el('div', 'ocl-section', unread.length ? (i18n.all ?? 'All conversations') : ''))\n for (const e of read) body.append(buildRow(e, false))\n }\n }\n\n const rowName = (entry: ChatListEntry): string =>\n entry.subjectTitle ?? (entry.kind === 'direct' ? (entry.peerId ?? 'Direct message') : 'General enquiry')\n\n const buildRow = (entry: ChatListEntry, unread: boolean): HTMLElement => {\n const name = rowName(entry)\n // Prefer the first LETTER (any script), not a leading digit/symbol — a\n // listing titled \"2018 Kia K7\" should show \"K\", not \"2\", and Korean/other\n // scripts pick their first character too.\n const initial = (name.match(/\\p{L}/u)?.[0] ?? name.trim()[0] ?? '?').toUpperCase()\n const lastSeq = entry.lastSeq ?? 0\n const unreadCount = unread ? Math.max(1, lastSeq - (seenSeq[entry.id] ?? 0)) : 0\n\n const row = el('button', `ocl-row${unread ? ' unread' : ''}`) as HTMLButtonElement\n\n // Avatar\n const av = el('div', 'ocl-av', initial)\n row.append(av)\n\n // Info\n const info = el('div', 'ocl-info')\n info.append(el('div', 'ocl-name', name))\n const stateMap: Record<string, string> = {\n open: 'Open', awaiting_staff: 'Waiting for reply…',\n resolved: 'Resolved ✓', closed: 'Closed',\n }\n info.append(el('div', 'ocl-preview', entry.lastMessage ?? stateMap[entry.state] ?? entry.state))\n row.append(info)\n\n // Right: timestamp, status chip, unread badge\n const right = el('div', 'ocl-right')\n right.append(el('div', 'ocl-time', timeAgo(entry.updatedAt)))\n const chip = statusChip(entry.state)\n if (chip) right.append(el('div', `ocl-status ${chip.cls}`, chip.label))\n if (unreadCount > 0) {\n right.append(el('div', 'ocl-badge', String(unreadCount > 99 ? '99+' : unreadCount)))\n }\n row.append(right)\n\n row.addEventListener('click', () => {\n // Mark as read\n if (lastSeq > 0) { seenSeq[entry.id] = lastSeq; saveSeenSeq() }\n row.classList.remove('unread')\n right.querySelector('.ocl-badge')?.remove()\n opts.onSelect(entry)\n })\n\n return row\n }\n\n const refresh = () => {\n if (destroyed) return\n fetchEntries().then(entries => {\n if (destroyed) return\n allEntries = entries\n renderRows(entries, searchIn.value.trim().toLowerCase())\n }).catch((e) => {\n if (destroyed) return\n console.error(`[chat-widget] failed to load conversations from ${httpBase}/conversations/mine — check the apiUrl/CORS config.`, e)\n const errBox = el('div', 'ocl-empty', i18n.error ?? 'Could not load conversations.')\n errBox.append(el('br'))\n const retry = el('button', 'ocl-retry', i18n.retry ?? 'Retry') as HTMLButtonElement\n retry.addEventListener('click', () => {\n body.replaceChildren(el('div', 'ocl-spinner', 'Loading…'))\n refresh()\n })\n errBox.append(retry)\n body.replaceChildren(errBox)\n })\n }\n\n searchIn.addEventListener('input', () => renderRows(allEntries, searchIn.value.trim().toLowerCase()))\n\n // Initial fetch\n refresh()\n\n // ── Live inbox: subscribe over WS so the list updates the instant any of the\n // guest's conversations changes, instead of only on the periodic poll. The\n // server streams `inbox_event` to the guest's OWN inbox (keyed by guestId).\n // We coalesce bursts and re-fetch (the fetch already sorts/dedupes); the poll\n // stays as a backstop for a dropped socket. ────────────────────────────────\n let sock: WebSocket | null = null\n let reconnectTimer: ReturnType<typeof setTimeout> | undefined\n let refreshTimer: ReturnType<typeof setTimeout> | undefined\n let attempt = 0\n let connectedBefore = false\n const debouncedRefresh = () => {\n if (refreshTimer) return\n refreshTimer = setTimeout(() => { refreshTimer = undefined; refresh() }, 300)\n }\n const connectInbox = () => {\n if (destroyed) return\n try { sock = new WebSocket(wsUrl) } catch { scheduleReconnect(); return }\n sock.binaryType = 'arraybuffer'\n sock.onopen = () => {\n attempt = 0\n sock!.send(encodeFrame({ type: 'auth', token }))\n sock!.send(encodeFrame({ type: 'subscribe_inbox' }))\n // On a RECONNECT (not the first connect — the initial mount already\n // fetched), catch up on anything that changed while the socket was down.\n if (connectedBefore) debouncedRefresh()\n connectedBefore = true\n }\n sock.onmessage = (ev) => {\n const frame = decodeFrame(new Uint8Array(ev.data as ArrayBuffer))\n // Any inbox change for this guest → refresh the list. `new` (a freshly\n // created thread) and `update` (new message / state) both apply.\n if (frame && frame.type === 'inbox_event') debouncedRefresh()\n }\n sock.onclose = () => { sock = null; scheduleReconnect() }\n sock.onerror = () => { try { sock?.close() } catch { /* noop */ } }\n }\n const scheduleReconnect = () => {\n if (destroyed || reconnectTimer) return\n const delay = Math.min(15_000, 500 * 2 ** attempt++) + Math.random() * 250\n reconnectTimer = setTimeout(() => { reconnectTimer = undefined; connectInbox() }, delay)\n }\n connectInbox()\n\n return {\n refresh,\n close() {\n destroyed = true\n if (reconnectTimer) clearTimeout(reconnectTimer)\n if (refreshTimer) clearTimeout(refreshTimer)\n compactObserver?.disconnect()\n compactObserver = null\n try { sock?.close() } catch { /* noop */ }\n sock = null\n opts.el.replaceChildren()\n },\n defaultProfileId() { return opts.profileId ?? serverDefaultProfileId },\n }\n}\n"],"names":["CSS","lightTokens","darkTokens","statusChip","state","timeAgo","ts","s","days","CHAT_ICON_SVG","el","tag","cls","text","e","mountChatList","opts","token","persistentUid","httpBase","wsUrl","resolveRelayUrls","i18n","accent","l","root","head","titleEl","compose","searchWrap","searchIn","body","applyCompact","w","compactObserver","entries","_a","serverDefaultProfileId","seenKey","seenSeq","saveSeenSeq","allEntries","destroyed","fetchEntries","who","url","headers","res","data","a","b","renderRows","query","filtered","rowName","empty","start","isUnread","unread","read","buildRow","entry","name","initial","lastSeq","unreadCount","row","av","info","stateMap","right","chip","refresh","errBox","retry","sock","reconnectTimer","refreshTimer","attempt","connectedBefore","debouncedRefresh","connectInbox","scheduleReconnect","encodeFrame","ev","frame","decodeFrame","delay"],"mappings":"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"}