useyuva 0.0.0-stage → 0.0.3

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 ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 productdevbook
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.
22
+
package/README.md CHANGED
@@ -1,3 +1,121 @@
1
- # Temporary Holding Version
1
+ # useyuva
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ JavaScript SDK for [Yuva](https://github.com/productdevbook/yuva), open-source customer messaging.
4
+
5
+ | Import | What |
6
+ |---|---|
7
+ | `useyuva` | `createYuvaClient()`: the contact side (`/client/v1`) without DOM. Browsers, Node 22+, Bun, Deno, workers. |
8
+ | `useyuva/chat` | The `<yuva-chat>` web component, built on that client. Browser only. |
9
+ | `useyuva/react` | `YuvaProvider`, `useConversations`, `useMessages`. React 18+ is an optional peer. |
10
+ | `useyuva/api` | A typed `/v1` client for backends (API key), generated from the OpenAPI contract. |
11
+
12
+ ```sh
13
+ npm install useyuva
14
+ ```
15
+
16
+ Without a bundler, the server hosts the widget itself: `<script src="https://<your-yuva>/yuva.js" defer></script>`.
17
+
18
+ ## Client
19
+
20
+ ```ts
21
+ import { createYuvaClient } from "useyuva";
22
+
23
+ const yuva = createYuvaClient({
24
+ server: "https://support.example.com",
25
+ channel: "yuva_pk_…",
26
+ identityToken: () => fetch("/my/yuva-token").then((r) => r.text()),
27
+ });
28
+
29
+ yuva.on("event", (event) => {
30
+ if (event.type === "message.created") console.log(event.data.author.type, event.data.body);
31
+ });
32
+ await yuva.connect();
33
+
34
+ const { conversation } = await yuva.startConversation({ body: "Hello" });
35
+ await yuva.sendMessage(conversation.id, { body: "Here is a screenshot", files: [file] });
36
+ ```
37
+
38
+ Options:
39
+
40
+ | Option | Meaning |
41
+ |---|---|
42
+ | `server` | Base URL of the Yuva server. |
43
+ | `channel` | Public key of a `chat` or `app` channel. Outside a browser use an `app` channel: `chat` channels accept only their allowed origins. |
44
+ | `identityToken` | `() => string \| null \| Promise<…>`: an identity token signed by your backend. Without it the contact is an anonymous visitor (if the channel allows them). |
45
+ | `storage` | `{ getItem, setItem }` keeping the session and visitor id. Defaults to `localStorage`, else memory. |
46
+ | `fetch`, `WebSocket` | Replacements for the global ones. |
47
+ | `idempotencyKeys` | Send `client_id` as `Idempotency-Key` too. On by default outside browsers; off in browsers until the server allows the header cross-origin. |
48
+
49
+ Session:
50
+
51
+ - `start()` opens a session, or resumes the stored one for the same identity. Every other call starts one when needed; a session that ended (`401`) is renewed once and the call retried.
52
+ - `session` (contact, inbox settings, expiry), `token`, `refresh()`, `channelSettings()` (the inbox's public settings without a session).
53
+ - `setIdentityToken(source)` switches identity; `reset()` forgets the session in memory; `signOut()` ends it on the server and in storage.
54
+
55
+ Conversations and messages:
56
+
57
+ - `listConversations({ cursor, limit })`, `getConversation(id)`, `startConversation({ body, subject, files, client_id })`.
58
+ - `listMessages(id, { order, cursor, limit })`, `sendMessage(id, { body, files, client_id })`. `client_id` (generated when absent) makes a retry safe: the server returns the stored message for a repeated `client_id`, and with `idempotencyKeys` the request also carries it as `Idempotency-Key`.
59
+ - `markRead(id, messageId?)`, `setTyping(id, typing)`, `setEmail(email)`, `attachment(id)` (a `Blob`).
60
+
61
+ Realtime:
62
+
63
+ - `connect()` opens `/client/v1/realtime`, resumes after the last event on reconnect, retries with backoff and renews an ended session. `disconnect()`, `reconnect()` (try now, e.g. when the page becomes visible), `connected`.
64
+ - `on(type, handler)` returns an unsubscribe function. `event`: every realtime frame (`message.created`, `conversation.updated`, `typing`, `presence`, `resync_required`, …). `connection`: `{ state: "connecting" | "open" | "reconnecting" | "closed", attempt }`. `session`: `{ session, renewed }`; after `renewed` or `resync_required`, reload what you show.
65
+
66
+ Errors are `ApiError` with `status` and the problem `code` (`status` 0 and `network` when offline).
67
+
68
+ ## Chat element
69
+
70
+ ```ts
71
+ import "useyuva/chat";
72
+ ```
73
+
74
+ ```html
75
+ <yuva-chat channel="yuva_pk_…" server="https://support.example.com"></yuva-chat>
76
+ ```
77
+
78
+ Attributes, methods and events are in [docs/widget.md](https://github.com/productdevbook/yuva/blob/main/docs/widget.md).
79
+
80
+ ## React
81
+
82
+ ```tsx
83
+ import { createYuvaClient } from "useyuva";
84
+ import { YuvaProvider, useConversations, useMessages } from "useyuva/react";
85
+
86
+ const yuva = createYuvaClient({ server, channel });
87
+
88
+ function App() {
89
+ return (
90
+ <YuvaProvider client={yuva}>
91
+ <Inbox />
92
+ </YuvaProvider>
93
+ );
94
+ }
95
+
96
+ function Thread({ id }: { id: string }) {
97
+ const { messages, send, loadOlder, hasMore } = useMessages(id);
98
+ // …
99
+ }
100
+ ```
101
+
102
+ `YuvaProvider` connects realtime while mounted (`connect={false}` to leave that to you).
103
+ `useConversations()` returns `{ conversations, loading, error, hasMore, loadMore, reload }`;
104
+ `useMessages(id)` returns `{ messages, loading, error, hasMore, loadOlder, send, markRead }`. Both
105
+ follow realtime events. Start a new conversation with `client.startConversation()`.
106
+
107
+ ## Typed /v1 client
108
+
109
+ ```ts
110
+ import { createYuvaApi } from "useyuva/api";
111
+
112
+ const api = createYuvaApi({ server: "https://support.example.com", apiKey: process.env.YUVA_API_KEY! });
113
+ const { data, error } = await api.GET("/v1/inboxes");
114
+ ```
115
+
116
+ Built on [openapi-fetch](https://openapi-ts.dev/openapi-fetch/); paths, parameters and bodies are
117
+ typed from the contract. Keep API keys on the server.
118
+
119
+ ## License
120
+
121
+ MIT
@@ -0,0 +1 @@
1
+ import createClient from"openapi-fetch";function createYuvaApi({server,apiKey,headers,...options}){return createClient({...options,baseUrl:server.replace(/\/+$/,""),headers:{...headers,Authorization:`Bearer ${apiKey}`}})}export{createYuvaApi};
@@ -0,0 +1,109 @@
1
+ import{launcherOf,readStored,writeStored}from"./index-r5vcg0sx.js";var messages=JSON.parse('{"6I7cJR":["Open chat"],"OkHvGB":["Open chat, ",["count"]," unread"],"pqZJT2":["Close chat"]}');var messages2=JSON.parse('{"6I7cJR":["Sohbeti aç"],"OkHvGB":["Sohbeti aç, ",["count"]," okunmamış"],"pqZJT2":["Sohbeti kapat"]}');var catalogs={en:messages,tr:messages2},rtlLanguages=new Set(["ar","fa","he","ur"]),sourceLocale="en";function resolveLocale(...candidates){for(let candidate of candidates){if(!candidate)continue;let tag=candidate.toLowerCase();if(catalogs[tag])return tag;let language=tag.split(/[-_]/)[0]??"";if(catalogs[language])return language}return sourceLocale}function directionOf(locale){return rtlLanguages.has(locale.split(/[-_]/)[0]??"")?"rtl":"ltr"}function catalogFor(locale){return catalogs[locale]??messages}function translatePlain(messages2,descriptor){let entry=messages2[descriptor.id]??messages[descriptor.id],values=descriptor.values??{};if(typeof entry==="string")return entry;if(Array.isArray(entry))return entry.map((part)=>typeof part==="string"?part:Array.isArray(part)?String(values[part[0]]??""):"").join("");return descriptor.message??descriptor.id}var styles=`
2
+ :host {
3
+ --yuva-accent: #2563eb;
4
+ --yuva-on-accent: #ffffff;
5
+ --yuva-bg: #ffffff;
6
+ --yuva-fg: #111827;
7
+ --yuva-muted: #6b7280;
8
+ --yuva-border: #e5e7eb;
9
+ --yuva-soft: #f3f4f6;
10
+ --yuva-danger: #dc2626;
11
+ --yuva-shadow: 0 12px 40px rgb(15 23 42 / 0.18);
12
+ color-scheme: light;
13
+ position: fixed;
14
+ bottom: 20px;
15
+ right: 20px;
16
+ z-index: 2147483000;
17
+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
18
+ font-size: 15px;
19
+ line-height: 1.45;
20
+ color: var(--yuva-fg);
21
+ }
22
+ :host([position="left"]) { right: auto; left: 20px; }
23
+ @media (prefers-color-scheme: dark) {
24
+ :host {
25
+ --yuva-bg: #1c1f24;
26
+ --yuva-fg: #f3f4f6;
27
+ --yuva-muted: #9ca3af;
28
+ --yuva-border: #30343b;
29
+ --yuva-soft: #2a2e35;
30
+ --yuva-danger: #f87171;
31
+ --yuva-shadow: 0 12px 40px rgb(0 0 0 / 0.5);
32
+ color-scheme: dark;
33
+ }
34
+ }
35
+ :host([layout="embedded"]) {
36
+ position: relative;
37
+ inset: auto;
38
+ z-index: auto;
39
+ display: block;
40
+ block-size: 100%;
41
+ }
42
+ .launcher {
43
+ position: relative;
44
+ display: grid;
45
+ place-items: center;
46
+ inline-size: 56px;
47
+ block-size: 56px;
48
+ padding: 0;
49
+ border: 0;
50
+ border-radius: 50%;
51
+ background: var(--yuva-accent);
52
+ color: var(--yuva-on-accent);
53
+ box-shadow: var(--yuva-shadow);
54
+ cursor: pointer;
55
+ }
56
+ .launcher:focus-visible { outline: 3px solid var(--yuva-accent); outline-offset: 3px; }
57
+ .badge {
58
+ position: absolute;
59
+ top: -4px;
60
+ right: -4px;
61
+ min-inline-size: 20px;
62
+ block-size: 20px;
63
+ padding-inline: 5px;
64
+ box-sizing: border-box;
65
+ border-radius: 10px;
66
+ background: var(--yuva-danger);
67
+ color: #fff;
68
+ font-size: 12px;
69
+ font-weight: 700;
70
+ line-height: 20px;
71
+ text-align: center;
72
+ }
73
+ .badge[hidden] { display: none; }
74
+ .panel {
75
+ display: flex;
76
+ flex-direction: column;
77
+ overflow: hidden;
78
+ box-sizing: border-box;
79
+ background: var(--yuva-bg);
80
+ border: 1px solid var(--yuva-border);
81
+ border-radius: 16px;
82
+ }
83
+ .panel[hidden] { display: none; }
84
+ :host(:not([layout="embedded"])) .panel {
85
+ position: absolute;
86
+ bottom: 72px;
87
+ right: 0;
88
+ inline-size: min(380px, calc(100vw - 32px));
89
+ block-size: min(600px, calc(100vh - 112px));
90
+ box-shadow: var(--yuva-shadow);
91
+ }
92
+ :host([position="left"]:not([layout="embedded"])) .panel { right: auto; left: 0; }
93
+ @media (max-width: 480px) {
94
+ :host(:not([layout="embedded"])) .panel {
95
+ position: fixed;
96
+ inset: 0;
97
+ inline-size: auto;
98
+ block-size: auto;
99
+ border: 0;
100
+ border-radius: 0;
101
+ }
102
+ :host([open-panel]:not([layout="embedded"])) .launcher { display: none; }
103
+ }
104
+ :host([layout="embedded"]) .panel {
105
+ inline-size: 100%;
106
+ block-size: 100%;
107
+ min-block-size: 320px;
108
+ }
109
+ `,chatIcon='<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" aria-hidden="true"><path d="M4 5h16v11H9l-5 4z"/></svg>',closeIcon='<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg>',scriptSrc=document.currentScript instanceof HTMLScriptElement?document.currentScript.src:"";function contrastOn(color){let n=Number.parseInt(color.slice(1),16),[r,g,b]=[n>>16&255,n>>8&255,n&255].map((c)=>{let s=c/255;return s<=0.03928?s/12.92:((s+0.055)/1.055)**2.4});return 0.2126*r+0.7152*g+0.0722*b>0.4?"#111827":"#ffffff"}class YuvaChatElement extends HTMLElement{static observedAttributes=["channel","server","layout","locale","dir","identity-token","open"];static loadPanel=()=>import(new URL("yuva-chat.js",scriptSrc||location.href).href);#root;#launcher=null;#badge=null;#panel;#controller=null;#messages={};#open=!1;#unread=0;#identity=null;#refused=!1;constructor(){super();this.#root=this.attachShadow({mode:"open"}),this.#panel=document.createElement("section"),this.#panel.className="panel",this.#panel.setAttribute("role","dialog"),this.#panel.hidden=!0,this.#panel.addEventListener("keydown",(event)=>{if(event.key==="Escape"&&this.layout==="launcher")this.close()})}get layout(){return this.getAttribute("layout")==="embedded"?"embedded":"launcher"}get channel(){return this.getAttribute("channel")}get server(){return(this.getAttribute("server")||(scriptSrc?new URL(scriptSrc).origin:location.origin)).replace(/\/+$/,"")}get locale(){return resolveLocale(this.getAttribute("locale"),navigator.language)}get isOpen(){return this.#open}get unread(){return this.#unread}setIdentityToken(source){if(this.#identity=source,this.#controller)this.#controller.then((controller)=>controller.identityChanged())}async signOut(){if(this.#identity=null,this.removeAttribute("identity-token"),this.#controller)await(await this.#controller).signOut()}connectedCallback(){this.#render();let channel=this.channel;if(this.layout==="embedded"||this.hasAttribute("open")||!channel)return;let stored=readStored(channel);if(stored.launcher)this.#applyLauncher(stored.launcher);let idle=()=>{if(!this.isConnected||this.#controller||this.channel!==channel)return;if(stored.session)this.#ensureController();else this.#styleLauncher(channel)};if("requestIdleCallback"in window)requestIdleCallback(idle,{timeout:3000});else setTimeout(idle,1000)}#styleLauncher(channel){fetch(`${this.server}/client/v1/channels/${encodeURIComponent(channel)}`,{credentials:"omit"}).then((response)=>{if(response.status===403||response.status===404)this.#refuse();return response.ok?response.json():null}).then((inbox)=>{if(!inbox||this.channel!==channel||this.#controller)return;let launcher=launcherOf(inbox);this.#applyLauncher(launcher),writeStored(channel,{...readStored(channel),launcher})}).catch(()=>{return})}disconnectedCallback(){let controller=this.#controller;if(this.#controller=null,controller)controller.then((c)=>c.destroy())}attributeChangedCallback(name,previous,value){if(!this.isConnected||previous===value)return;if(name==="open"){value===null?this.close():this.open();return}if(name==="identity-token"){if(this.#controller)this.#controller.then((controller)=>controller.identityChanged());return}if(name==="channel"||name==="server"){this.disconnectedCallback(),this.#refused=!1,this.#render();return}if(this.#render(),this.#controller)this.#controller.then((controller)=>controller.update())}async open(){if(this.#open||this.#refused)return;this.#open=!0,this.toggleAttribute("open-panel",!0),this.#syncLauncher();let controller=await this.#ensureController();if(!this.#open)return;this.#panel.hidden=!1,controller.opened()}close(){if(!this.#open||this.layout==="embedded")return;if(this.#open=!1,this.toggleAttribute("open-panel",!1),this.#panel.hidden=!0,this.#syncLauncher(),this.#launcher?.focus(),this.#controller)this.#controller.then((controller)=>controller.closed())}toggle(){return this.#open?this.close():this.open()}#config(){return{channel:this.channel,server:this.server,layout:this.layout,locale:this.locale}}#ensureController(){return this.#controller??=YuvaChatElement.loadPanel().then(({mount})=>mount({panel:this.#panel,config:()=>this.#config(),identityToken:async()=>this.#identity?await this.#identity()??null:this.getAttribute("identity-token"),isOpen:()=>this.#open,close:()=>this.close(),setUnread:(count)=>{this.#unread=count,this.#syncLauncher(),this.dispatchEvent(new CustomEvent("yuva-unread",{detail:{count}}))},setLauncher:(style)=>this.#applyLauncher(style),refused:()=>this.#refuse()})),this.#controller}#refuse(){if(this.#refused)return;this.#refused=!0,console.warn(`Yuva: channel ${this.channel} is unknown or does not allow ${location.origin}`),this.#open=!1,this.toggleAttribute("open-panel",!1),this.disconnectedCallback(),this.#render()}#applyLauncher(style){if(this.setAttribute("position",style.position==="left"?"left":"right"),style.color&&/^#[0-9a-f]{6}$/i.test(style.color))this.style.setProperty("--yuva-accent",style.color),this.style.setProperty("--yuva-on-accent",contrastOn(style.color))}#render(){let locale=this.locale;this.#messages=catalogFor(locale),this.setAttribute("lang",locale);let style=document.createElement("style");if(style.textContent=styles,this.#panel.dir=this.getAttribute("dir")??directionOf(locale),this.#panel.setAttribute("aria-modal","false"),this.#refused){this.#launcher=null,this.#badge=null,this.#root.replaceChildren();return}if(this.layout==="embedded"){this.#launcher=null,this.#badge=null,this.#root.replaceChildren(style,this.#panel),this.#open=!1,this.open();return}let launcher=document.createElement("button");if(launcher.type="button",launcher.className="launcher",launcher.addEventListener("click",()=>void this.toggle()),this.#launcher=launcher,this.#badge=document.createElement("span"),this.#badge.className="badge",this.#badge.setAttribute("aria-hidden","true"),this.#root.replaceChildren(style,this.#panel,launcher),this.#syncLauncher(),this.hasAttribute("open")&&!this.#open)this.open()}#syncLauncher(){let launcher=this.#launcher,badge=this.#badge;if(!launcher||!badge)return;let count=this.#unread;launcher.setAttribute("aria-expanded",String(this.#open)),launcher.setAttribute("aria-label",this.#open?translatePlain(this.#messages,{id:"pqZJT2"}):count>0?translatePlain(this.#messages,{id:"OkHvGB",values:{count}}):translatePlain(this.#messages,{id:"6I7cJR"})),launcher.innerHTML=this.#open?closeIcon:chatIcon,badge.textContent=count>9?"9+":String(count),badge.hidden=this.#open||count===0,launcher.append(badge)}}function define(){if(!customElements.get("yuva-chat"))customElements.define("yuva-chat",YuvaChatElement)}YuvaChatElement.loadPanel=()=>import("./yuva-chat-2482m97r.js");define();export{YuvaChatElement,define};
@@ -0,0 +1,12 @@
1
+ class ApiError2 extends Error{status;code;constructor(status,code){super(`${status} ${code}`);this.status=status;this.code=code}}async function call(fetcher,server,path,{method="GET",json,body,headers={},token,blob}={}){if(headers={...headers},token)headers.Authorization=`Bearer ${token}`;let payload=body;if(json!==void 0)headers["Content-Type"]="application/json",payload=JSON.stringify(json);let response;try{response=await fetcher(server+path,{method,headers,body:payload,credentials:"omit"})}catch{throw new ApiError2(0,"network")}if(!response.ok){let code="error";try{code=(await response.json()).code??code}catch{}throw new ApiError2(response.status,code)}if(response.status===204)return;return blob?await response.blob():await response.json()}class Realtime{server;socketClass;handlers;#socket=null;#lastEventId=0;#attempt=0;#timer=null;#stopped=!0;constructor(server,socketClass,handlers){this.server=server;this.socketClass=socketClass;this.handlers=handlers}get connected(){return this.#socket?.readyState===1}start(){if(this.#stopped=!1,!this.#socket)this.#connect()}stop(){if(this.#stopped=!0,this.#timer)clearTimeout(this.#timer);this.#timer=null;let socket=this.#socket;this.#socket=null,socket?.close(1000)}reset(){this.#lastEventId=0}nudge(){if(this.#stopped||this.#socket)return;if(this.#timer)clearTimeout(this.#timer);this.#timer=null,this.#connect()}#connect(){let token=this.handlers.token();if(!token||this.#stopped)return;let url=new URL(`${this.server}/client/v1/realtime`,globalThis.location?.href);if(url.protocol=url.protocol==="https:"?"wss:":"ws:",this.#lastEventId>0)url.searchParams.set("last_event_id",String(this.#lastEventId));let socket;try{socket=new this.socketClass(url,["yuva",`yuva.token.${token}`])}catch{this.#schedule();return}this.#socket=socket,socket.onmessage=(event)=>{let message;try{message=JSON.parse(String(event.data))}catch{return}if(message.type==="ready")this.#attempt=0,this.#lastEventId=Math.max(this.#lastEventId,message.last_event_id);else if("id"in message&&typeof message.id==="number")this.#lastEventId=Math.max(this.#lastEventId,message.id);this.handlers.event(message)},socket.onclose=(event)=>{if(this.#socket!==socket)return;if(this.#socket=null,this.#stopped)return;if(event.code===1008){this.#lastEventId=0,this.handlers.sessionEnded();return}this.#schedule()}}#schedule(){if(this.#stopped||this.#timer)return;let attempt=this.#attempt++;this.handlers.reconnecting(attempt);let delay=Math.min(30000,1000*2**attempt)*(0.75+Math.random()*0.5);this.#timer=setTimeout(()=>{this.#timer=null,this.#connect()},delay)}}function storageKey(channel){return`yuva:${channel}`}function randomId(){if(typeof crypto.randomUUID==="function")return crypto.randomUUID();return Array.from(crypto.getRandomValues(new Uint8Array(16)),(b)=>b.toString(16).padStart(2,"0")).join("")}function jwtSubject(token){try{let part=(token.split(".")[1]??"").replace(/-/g,"+").replace(/_/g,"/"),payload=JSON.parse(atob(part));return typeof payload.sub==="string"?payload.sub:null}catch{return null}}function defaultStorage(){try{let storage=globalThis.localStorage;if(storage)return storage}catch{}let memory=new Map;return{getItem:(key)=>memory.get(key)??null,setItem:(key,value)=>void memory.set(key,value)}}function query(params){let search=new URLSearchParams;for(let[name,value]of Object.entries(params))if(value!==void 0)search.set(name,String(value));let text=search.toString();return text?`?${text}`:""}function quoted(value){return value.replace(/"/g,"%22").replace(/\r/g,"%0D").replace(/\n/g,"%0A")}function multipart(fields,files,boundary){let parts=[];for(let[name,value]of fields)parts.push(`--${boundary}\r
2
+ Content-Disposition: form-data; name="${name}"\r
3
+ \r
4
+ ${value}\r
5
+ `);for(let file of files){let filename=file.name||"file";parts.push(`--${boundary}\r
6
+ Content-Disposition: form-data; name="files"; filename="${quoted(filename)}"\r
7
+ Content-Type: ${file.type||"application/octet-stream"}\r
8
+ \r
9
+ `,file,`\r
10
+ `)}return parts.push(`--${boundary}--\r
11
+ `),new Blob(parts)}class YuvaClient2{server;channel;#fetch;#socketClass;#storage;#identity;#idempotencyKeys;#token=null;#session=null;#starting=null;#renewing=null;#realtime=null;#handlers=new Map;constructor(options){this.server=options.server.replace(/\/+$/,""),this.channel=options.channel,this.#fetch=options.fetch??((url,init)=>fetch(url,init)),this.#socketClass=options.WebSocket??globalThis.WebSocket,this.#storage=options.storage??defaultStorage(),this.#identity=options.identityToken??null,this.#idempotencyKeys=options.idempotencyKeys??typeof globalThis.document>"u"}get session(){return this.#session}get token(){return this.#token}get connected(){return this.#realtime?.connected??!1}on(type,handler){let set=this.#handlers.get(type);if(!set)this.#handlers.set(type,set=new Set);return set.add(handler),()=>void set.delete(handler)}#emit(type,value){for(let handler of this.#handlers.get(type)??[])handler(value)}stored(){try{let raw=this.#storage.getItem(storageKey(this.channel));return raw?JSON.parse(raw):{}}catch{return{}}}#store(state){try{this.#storage.setItem(storageKey(this.channel),JSON.stringify(state))}catch{}}channelSettings(){return this.#call(`/client/v1/channels/${encodeURIComponent(this.channel)}`)}setIdentityToken(source){this.#identity=source,this.reset()}start(){if(this.#session)return Promise.resolve(this.#session);return this.#starting??=this.#open(!1).then((session)=>(this.#emit("session",{session,renewed:!1}),session)).finally(()=>{this.#starting=null}),this.#starting}async refresh(){let info=await this.#authed("/client/v1/session");return this.#session={...this.#session,...info},this.#emit("session",{session:this.#session,renewed:!1}),this.#session}reset(){this.#realtime?.stop(),this.#realtime?.reset(),this.#starting=null,this.#renewing=null,this.#token=null,this.#session=null}async signOut(){let token=this.#token;this.reset();let stored=this.stored();if(delete stored.session,this.#store(stored),token)await this.#call("/client/v1/session",{method:"DELETE",token}).catch(()=>{return})}async connect(){if(!this.#socketClass)throw Error("Yuva: no WebSocket in this runtime; pass options.WebSocket");if(await this.start(),this.#realtime??=new Realtime(this.server,this.#socketClass,{token:()=>this.#token,event:(message)=>{if(message.type==="ready")this.#emit("connection",{state:"open",attempt:0});this.#emit("event",message)},sessionEnded:()=>void this.#renew().catch(()=>this.#realtime?.nudge()),reconnecting:(attempt)=>{if(this.#emit("connection",{state:"reconnecting",attempt}),attempt===2)this.#authed("/client/v1/session").catch(()=>{return})}}),!this.#realtime.connected)this.#emit("connection",{state:"connecting",attempt:0});this.#realtime.start()}disconnect(){this.#realtime?.stop(),this.#emit("connection",{state:"closed",attempt:0})}reconnect(){this.#realtime?.nudge()}listConversations(page={}){return this.#authed(`/client/v1/conversations${query({...page})}`)}getConversation(id){return this.#authed(`/client/v1/conversations/${encodeURIComponent(id)}`)}startConversation(input){return this.#authed("/client/v1/conversations",this.#messageRequest(input))}listMessages(conversationId,page={}){return this.#authed(`/client/v1/conversations/${encodeURIComponent(conversationId)}/messages${query({...page})}`)}sendMessage(conversationId,input){return this.#authed(`/client/v1/conversations/${encodeURIComponent(conversationId)}/messages`,this.#messageRequest(input))}markRead(conversationId,messageId){return this.#authed(`/client/v1/conversations/${encodeURIComponent(conversationId)}/read`,{method:"POST",json:messageId?{message_id:messageId}:{}})}setTyping(conversationId,typing){return this.#authed(`/client/v1/conversations/${encodeURIComponent(conversationId)}/typing`,{method:"POST",json:{typing}})}setEmail(email){return this.#authed("/client/v1/contact/email",{method:"PUT",json:{email}})}attachment(id){return this.#authed(`/client/v1/attachments/${encodeURIComponent(id)}`,{blob:!0})}#messageRequest({body,subject,files=[],client_id=randomId()}){let headers=this.#idempotencyKeys&&/^[\x20-\x7e]{1,255}$/.test(client_id)?{"Idempotency-Key":client_id}:{};if(files.length===0)return{method:"POST",headers,json:{body,subject,client_id}};let fields=[];if(subject)fields.push(["subject",subject]);if(body)fields.push(["body",body]);fields.push(["client_id",client_id]);let boundary=`yuva-${client_id.replace(/[^\w-]/g,"").slice(0,60)}-${client_id.length}`;return headers["Content-Type"]=`multipart/form-data; boundary=${boundary}`,{method:"POST",headers,body:multipart(fields,files,boundary)}}#call(path,request){return call(this.#fetch,this.server,path,request)}async#authed(path,request={},retry=!0){if(!this.#token)await this.start();try{return await this.#call(path,{...request,token:this.#token})}catch(error){if(!retry||!(error instanceof ApiError2)||error.status!==401)throw error;return await this.#renew(),this.#authed(path,request,!1)}}#renew(){return this.#renewing??=(async()=>{let session=await this.#open(!0);if(this.#realtime)this.#realtime.stop(),this.#realtime.reset(),this.#realtime.start();this.#emit("session",{session,renewed:!0})})().finally(()=>{this.#renewing=null}),this.#renewing}async#open(fresh){let stored=this.stored(),identity=await this.#identity?.()||null,subject=identity?jwtSubject(identity):null,saved=stored.session;if(!fresh&&saved&&saved.sub===subject&&Date.parse(saved.expires_at)>Date.now()+60000)try{let info=await this.#call("/client/v1/session",{token:saved.token});return this.#apply(info,saved.token,subject)}catch(error){if(!(error instanceof ApiError2)||error.status!==401&&error.status!==403)throw error}let session=await this.#call("/client/v1/session",{method:"POST",json:{channel_key:this.channel,identity_token:identity??void 0,visitor_id:stored.visitor_id}}),{token,...info}=session;return this.#apply(info,token,subject)}#apply(info,token,subject){this.#token=token,this.#session=info;let stored=this.stored();if(stored.session={token,expires_at:info.expires_at,sub:subject},info.visitor_id)stored.visitor_id=info.visitor_id;else if(info.contact.identified)delete stored.visitor_id;return this.#store(stored),info}}function createYuvaClient2(options){return new YuvaClient2(options)}
12
+ export{ApiError2,randomId,YuvaClient2,createYuvaClient2};
@@ -0,0 +1,2 @@
1
+ function launcherOf(inbox){return{position:inbox.chat.launcher_position,color:inbox.chat.launcher_color??inbox.branding.color}}function storageKey(channel){return`yuva:${channel}`}function readStored(channel){try{let raw=localStorage.getItem(storageKey(channel));return raw?JSON.parse(raw):{}}catch{return{}}}function writeStored(channel,state){try{localStorage.setItem(storageKey(channel),JSON.stringify(state))}catch{}}
2
+ export{launcherOf,readStored,writeStored};
@@ -0,0 +1 @@
1
+ import{ApiError2,YuvaClient2,createYuvaClient2}from"./index-feftwnsj.js";export{ApiError2 as ApiError,YuvaClient2 as YuvaClient,createYuvaClient2 as createYuvaClient};
@@ -0,0 +1 @@
1
+ import{createContext,createElement,useCallback,useContext,useEffect,useRef,useState}from"react";var YuvaContext=createContext(null);function YuvaProvider({client,connect=!0,children}){return useEffect(()=>{if(!connect)return;return client.connect().catch(()=>{return}),()=>client.disconnect()},[client,connect]),createElement(YuvaContext.Provider,{value:client},children)}function useYuva(){let client=useContext(YuvaContext);if(!client)throw Error("Yuva: wrap your components in <YuvaProvider client={...}>");return client}var time=(value)=>Date.parse(value),byCreated=(a,b)=>time(a.created_at)-time(b.created_at),byActivity=(a,b)=>time(b.last_message_at??b.created_at)-time(a.last_message_at??a.created_at),initial={items:[],loading:!0,error:null};function useConversations(){let client=useYuva(),[state,setState]=useState(initial),cursor=useRef(void 0),current=useRef(state);current.current=state;let load=useCallback(async(more)=>{setState((s)=>({...s,loading:!0}));try{let page=await client.listConversations({limit:50,cursor:more?cursor.current:void 0});cursor.current=page.next_cursor,setState((s)=>{let items=new Map((more?s.items:[]).map((c)=>[c.id,c]));for(let conversation of page.items)items.set(conversation.id,conversation);return{items:[...items.values()].sort(byActivity),cursor:page.next_cursor,loading:!1,error:null}})}catch(error){setState((s)=>({...s,loading:!1,error}))}},[client]);return useEffect(()=>{load(!1);let offSession=client.on("session",({renewed})=>void(renewed&&load(!1))),offEvent=client.on("event",(message)=>{if(message.type==="resync_required")return void load(!1);if(message.type==="conversation.created")setState((s)=>s.items.some((c)=>c.id===message.data.id)?s:{...s,items:[message.data,...s.items].sort(byActivity)});else if(message.type==="conversation.updated")setState((s)=>({...s,items:s.items.map((c)=>c.id===message.conversation_id?{...c,status:message.data.status}:c)}));else if(message.type==="message.created"){let m=message.data;if(!current.current.items.some((c)=>c.id===m.conversation_id))return void load(!1);setState((s)=>{let items=s.items.map((c)=>c.id===m.conversation_id?{...c,last_message:{id:m.id,author_type:m.author.type,text:m.body.slice(0,140),created_at:m.created_at},last_message_at:m.created_at,unread:c.unread||m.author.type!=="contact"}:c);return{...s,items:items.sort(byActivity)}})}});return()=>{offSession(),offEvent()}},[client,load]),{conversations:state.items,loading:state.loading,error:state.error,hasMore:!!state.cursor,loadMore:()=>load(!0),reload:()=>load(!1)}}function useMessages(conversationId){let client=useYuva(),[state,setState]=useState(initial),cursor=useRef(void 0),upsert=useCallback((message)=>{setState((s)=>{let items=s.items.filter((m)=>m.id!==message.id&&(!message.client_id||m.client_id!==message.client_id));return{...s,items:[...items,message].sort(byCreated)}})},[]),load=useCallback(async(older)=>{if(!conversationId){cursor.current=void 0,setState({items:[],loading:!1,error:null});return}if(older&&!cursor.current)return;setState((s)=>({...s,loading:!0}));try{let page=await client.listMessages(conversationId,{order:"desc",limit:30,cursor:older?cursor.current:void 0});cursor.current=page.next_cursor,setState((s)=>{let items=new Map((older?s.items:[]).map((m)=>[m.id,m]));for(let message of page.items)items.set(message.id,message);return{items:[...items.values()].sort(byCreated),cursor:page.next_cursor,loading:!1,error:null}})}catch(error){setState((s)=>({...s,loading:!1,error}))}},[client,conversationId]);return useEffect(()=>{if(load(!1),!conversationId)return;let offSession=client.on("session",({renewed})=>void(renewed&&load(!1))),offEvent=client.on("event",(message)=>{if(message.type==="resync_required")load(!1);else if((message.type==="message.created"||message.type==="message.updated")&&message.data.conversation_id===conversationId)upsert(message.data)});return()=>{offSession(),offEvent()}},[client,conversationId,load,upsert]),{messages:state.items,loading:state.loading,error:state.error,hasMore:!!state.cursor,loadOlder:()=>load(!0),async send(input){if(!conversationId)throw Error("Yuva: no conversation; start one with client.startConversation()");let message=await client.sendMessage(conversationId,typeof input==="string"?{body:input}:input);return upsert(message),message},async markRead(){if(conversationId)await client.markRead(conversationId)}}}export{YuvaProvider,useConversations,useMessages,useYuva};
@@ -0,0 +1,183 @@
1
+ import{ApiError2,randomId,createYuvaClient2}from"./index-feftwnsj.js";import{launcherOf,readStored,writeStored}from"./index-r5vcg0sx.js";import{setupI18n}from"@lingui/core";var messages=JSON.parse(`{"-HY5kX":["New conversation"],"09Tv5j":["Reconnecting…"],"0XGLbP":["Your sign-in has expired. Reload the page and try again."],"1EICzz":["We're away right now"],"6gRgw8":["Retry"],"6lGaum":["Seen"],"ASr-r0":["Show older conversations"],"Amai22":["Chat is not available right now."],"E_qvzP":["Enter a valid e-mail address."],"GQVIIl":["You seem to be offline."],"JkW3Ph":["This file type is not accepted."],"JlFcis":["Send"],"KDw4GX":["Try again"],"M62PBt":["Nobody is available right now. Leave your e-mail and we'll send the reply there too."],"N7m8E-":["Attach files"],"Pwqkdw":["Loading…"],"PxYAul":["Typing…"],"RzWUUK":["Write a message…"],"S63MQu":["All conversations"],"Sad2tK":["Sending…"],"SdV5Cn":["Send us a message and we will reply here."],"UY1vmE":["Attachment"],"VggdQJ":["Usually replies in ",["duration"]],"XJURKP":["E-mail address"],"YyNOE7":["Conversation"],"ZxJcgC":["Could not save your e-mail address. Try again."],"eOu9Px":["The file is too large."],"h69WC6":["Sent"],"iGNUZJ":["Thanks! We'll e-mail you at ",["known"]," if you're gone when we reply."],"meAa31":["No conversations yet"],"nhTADq":["We'll reply as soon as we can"],"njBr8W":["you@example.com"],"oXOSPE":["Online"],"t7TeQU":["Messages"],"tHaDPf":[["name"]," is typing…"],"tfDRzk":["Save"],"tzsBOP":["Too many attempts. Wait a moment and try again."],"upFvcp":["Not sent."],"w61W3L":["Remove ",["0"]],"xDAtGP":["Message"],"ydzYKJ":["You:"],"yz7wBu":["Close"],"zLzEI3":["Sign in to chat with us."]}`);var messages2=JSON.parse('{"-HY5kX":["Yeni konuşma"],"09Tv5j":["Yeniden bağlanıyor…"],"0XGLbP":["Oturumunuzun süresi doldu. Sayfayı yenileyip tekrar deneyin."],"1EICzz":["Şu anda uzaktayız"],"6gRgw8":["Tekrar dene"],"6lGaum":["Görüldü"],"ASr-r0":["Eski konuşmaları göster"],"Amai22":["Sohbet şu anda kullanılamıyor."],"E_qvzP":["Geçerli bir e-posta adresi girin."],"GQVIIl":["Çevrimdışı görünüyorsunuz."],"JkW3Ph":["Bu dosya türü kabul edilmiyor."],"JlFcis":["Gönder"],"KDw4GX":["Tekrar dene"],"M62PBt":["Şu anda müsait kimse yok. E-posta adresinizi bırakın, yanıtı oraya da gönderelim."],"N7m8E-":["Dosya ekle"],"Pwqkdw":["Yükleniyor…"],"PxYAul":["Yazıyor…"],"RzWUUK":["Bir mesaj yazın…"],"S63MQu":["Tüm konuşmalar"],"Sad2tK":["Gönderiliyor…"],"SdV5Cn":["Bize bir mesaj gönderin, yanıtımız burada görünecek."],"UY1vmE":["Ek"],"VggdQJ":["Genellikle ",["duration"]," içinde yanıt verir"],"XJURKP":["E-posta adresi"],"YyNOE7":["Konuşma"],"ZxJcgC":["E-posta adresiniz kaydedilemedi. Tekrar deneyin."],"eOu9Px":["Dosya çok büyük."],"h69WC6":["Gönderildi"],"iGNUZJ":["Teşekkürler! Yanıtladığımızda burada değilseniz ",["known"]," adresine e-posta göndereceğiz."],"meAa31":["Henüz konuşma yok"],"nhTADq":["En kısa sürede yanıt vereceğiz"],"njBr8W":["siz@ornek.com"],"oXOSPE":["Çevrimiçi"],"t7TeQU":["Mesajlar"],"tHaDPf":[["name"]," yazıyor…"],"tfDRzk":["Kaydet"],"tzsBOP":["Çok fazla deneme. Biraz bekleyip tekrar deneyin."],"upFvcp":["Gönderilemedi."],"w61W3L":[["0"]," kaldır"],"xDAtGP":["Mesaj"],"ydzYKJ":["Siz:"],"yz7wBu":["Kapat"],"zLzEI3":["Bizimle sohbet etmek için giriş yapın."]}');var styles=`
2
+ [hidden] { display: none !important; }
3
+ .header {
4
+ display: flex;
5
+ align-items: center;
6
+ gap: 10px;
7
+ padding-block: 12px;
8
+ padding-inline: 14px 10px;
9
+ background: var(--yuva-accent);
10
+ color: var(--yuva-on-accent);
11
+ }
12
+ .heading { flex: 1; min-inline-size: 0; }
13
+ .title {
14
+ margin: 0;
15
+ font-size: 16px;
16
+ font-weight: 600;
17
+ overflow: hidden;
18
+ text-overflow: ellipsis;
19
+ white-space: nowrap;
20
+ }
21
+ .status { display: flex; align-items: center; gap: 6px; font-size: 13px; opacity: 0.9; }
22
+ .dot { inline-size: 8px; block-size: 8px; border-radius: 50%; background: #22c55e; box-shadow: 0 0 0 2px rgb(255 255 255 / 0.6); }
23
+ .dot.away { background: #9ca3af; }
24
+ .avatars { display: flex; }
25
+ .avatars .avatar { margin-inline-start: -8px; box-shadow: 0 0 0 2px var(--yuva-accent); }
26
+ .avatars .avatar:first-child { margin-inline-start: 0; }
27
+ .avatar {
28
+ display: grid;
29
+ place-items: center;
30
+ flex: none;
31
+ inline-size: 28px;
32
+ block-size: 28px;
33
+ border-radius: 50%;
34
+ background: var(--yuva-soft);
35
+ color: var(--yuva-fg);
36
+ font-size: 11px;
37
+ font-weight: 700;
38
+ text-transform: uppercase;
39
+ }
40
+ .icon {
41
+ display: grid;
42
+ place-items: center;
43
+ flex: none;
44
+ inline-size: 32px;
45
+ block-size: 32px;
46
+ padding: 0;
47
+ border: 0;
48
+ border-radius: 8px;
49
+ background: transparent;
50
+ color: inherit;
51
+ cursor: pointer;
52
+ }
53
+ .icon:hover { background: rgb(127 127 127 / 0.18); }
54
+ .icon:focus-visible { outline: 2px solid currentColor; outline-offset: -4px; }
55
+ [dir="rtl"] .flip { transform: scaleX(-1); }
56
+ .body { position: relative; flex: 1; min-block-size: 0; display: flex; flex-direction: column; }
57
+ .scroll { flex: 1; overflow-y: auto; overscroll-behavior: contain; padding: 14px; display: flex; flex-direction: column; gap: 4px; }
58
+ .center { flex: 1; display: grid; place-content: center; gap: 6px; padding: 24px; text-align: center; }
59
+ .empty { margin: 0; font-weight: 600; }
60
+ .hint { margin: 0; color: var(--yuva-muted); font-size: 14px; }
61
+ .list { list-style: none; margin: 0; padding: 6px; overflow-y: auto; flex: 1; }
62
+ .item {
63
+ display: flex;
64
+ gap: 10px;
65
+ align-items: center;
66
+ inline-size: 100%;
67
+ padding: 10px;
68
+ border: 0;
69
+ border-radius: 10px;
70
+ background: transparent;
71
+ color: inherit;
72
+ font: inherit;
73
+ text-align: start;
74
+ cursor: pointer;
75
+ }
76
+ .item:hover, .item:focus-visible { background: var(--yuva-soft); outline: none; }
77
+ .item-main { flex: 1; min-inline-size: 0; }
78
+ .item-top { display: flex; gap: 8px; justify-content: space-between; }
79
+ .item-subject, .item-preview { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
80
+ .item-subject { font-weight: 600; }
81
+ .item-preview { color: var(--yuva-muted); font-size: 14px; }
82
+ .item.unread .item-preview { color: var(--yuva-fg); font-weight: 600; }
83
+ .item-time { flex: none; color: var(--yuva-muted); font-size: 12px; }
84
+ .unread-dot { flex: none; inline-size: 9px; block-size: 9px; border-radius: 50%; background: var(--yuva-accent); }
85
+ .primary {
86
+ margin: 10px;
87
+ padding: 10px 14px;
88
+ border: 0;
89
+ border-radius: 10px;
90
+ background: var(--yuva-accent);
91
+ color: var(--yuva-on-accent);
92
+ font: inherit;
93
+ font-weight: 600;
94
+ cursor: pointer;
95
+ }
96
+ .row { display: flex; gap: 8px; align-items: flex-end; max-inline-size: 85%; }
97
+ .row.mine { align-self: flex-end; }
98
+ .row.theirs .avatar { margin-block-end: 2px; }
99
+ .avatar img { inline-size: 100%; block-size: 100%; border-radius: 50%; object-fit: cover; }
100
+ .group-gap { margin-block-start: 8px; }
101
+ .author { margin: 6px 0 2px; margin-inline-start: 36px; color: var(--yuva-muted); font-size: 12px; }
102
+ .bubble {
103
+ padding: 8px 12px;
104
+ border-radius: 16px;
105
+ background: var(--yuva-soft);
106
+ overflow-wrap: anywhere;
107
+ white-space: normal;
108
+ }
109
+ .mine .bubble { background: var(--yuva-accent); color: var(--yuva-on-accent); border-end-end-radius: 4px; }
110
+ .theirs .bubble { border-end-start-radius: 4px; }
111
+ .bubble a { color: inherit; text-decoration: underline; }
112
+ .bubble code { font-family: ui-monospace, Menlo, monospace; font-size: 13px; padding: 1px 4px; border-radius: 4px; background: rgb(127 127 127 / 0.2); }
113
+ .bubble pre { margin: 4px 0; padding: 8px; border-radius: 8px; background: rgb(127 127 127 / 0.2); font-family: ui-monospace, Menlo, monospace; font-size: 13px; white-space: pre-wrap; }
114
+ .greeting { align-self: flex-start; max-inline-size: 85%; }
115
+ .files { display: flex; flex-direction: column; gap: 4px; margin-block-start: 4px; }
116
+ .files img { display: block; max-inline-size: 100%; max-block-size: 220px; border-radius: 10px; cursor: zoom-in; }
117
+ .file {
118
+ display: inline-flex;
119
+ gap: 6px;
120
+ align-items: center;
121
+ padding: 6px 10px;
122
+ border: 1px solid currentColor;
123
+ border-radius: 8px;
124
+ background: transparent;
125
+ color: inherit;
126
+ font: inherit;
127
+ font-size: 13px;
128
+ cursor: pointer;
129
+ }
130
+ .meta { align-self: flex-end; color: var(--yuva-muted); font-size: 12px; margin-block-start: 2px; }
131
+ .meta.failed { color: var(--yuva-danger); }
132
+ .link { padding: 0; border: 0; background: none; color: inherit; font: inherit; text-decoration: underline; cursor: pointer; }
133
+ .typing { display: flex; gap: 6px; align-items: center; color: var(--yuva-muted); font-size: 13px; margin-block-start: 6px; }
134
+ .typing i { display: inline-block; inline-size: 6px; block-size: 6px; border-radius: 50%; background: currentColor; animation: blink 1.2s infinite both; }
135
+ .typing i:nth-child(2) { animation-delay: 0.2s; }
136
+ .typing i:nth-child(3) { animation-delay: 0.4s; }
137
+ @keyframes blink { 0%, 80%, 100% { opacity: 0.25; } 40% { opacity: 1; } }
138
+ @media (prefers-reduced-motion: reduce) { .typing i { animation: none; } }
139
+ .card { margin-block-start: 10px; padding: 12px; border: 1px solid var(--yuva-border); border-radius: 12px; font-size: 14px; }
140
+ .card p { margin: 0 0 8px; }
141
+ .card form { display: flex; gap: 6px; }
142
+ .card input {
143
+ flex: 1;
144
+ min-inline-size: 0;
145
+ padding: 8px 10px;
146
+ border: 1px solid var(--yuva-border);
147
+ border-radius: 8px;
148
+ background: var(--yuva-bg);
149
+ color: inherit;
150
+ font: inherit;
151
+ }
152
+ .card button { padding: 8px 12px; border: 0; border-radius: 8px; background: var(--yuva-accent); color: var(--yuva-on-accent); font: inherit; font-weight: 600; cursor: pointer; }
153
+ .card .error { margin: 6px 0 0; color: var(--yuva-danger); }
154
+ .composer { border-block-start: 1px solid var(--yuva-border); padding: 8px; }
155
+ .composer form { display: flex; gap: 4px; align-items: flex-end; }
156
+ .composer textarea {
157
+ flex: 1;
158
+ min-inline-size: 0;
159
+ box-sizing: border-box;
160
+ block-size: 38px;
161
+ max-block-size: 140px;
162
+ padding: 8px 10px;
163
+ border: 0;
164
+ border-radius: 10px;
165
+ background: transparent;
166
+ color: inherit;
167
+ font: inherit;
168
+ resize: none;
169
+ outline: none;
170
+ }
171
+ .composer .icon { color: var(--yuva-muted); }
172
+ .composer .send { color: var(--yuva-accent); }
173
+ .composer .send:disabled { opacity: 0.4; cursor: default; }
174
+ .chips { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 6px 6px; }
175
+ .chip { display: inline-flex; gap: 4px; align-items: center; max-inline-size: 100%; padding: 2px 4px 2px 10px; border-radius: 999px; background: var(--yuva-soft); font-size: 13px; }
176
+ .chip span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-inline-size: 180px; }
177
+ .chip .icon { inline-size: 22px; block-size: 22px; }
178
+ .banner { padding: 6px 12px; background: var(--yuva-soft); color: var(--yuva-muted); font-size: 13px; text-align: center; }
179
+ @media (max-width: 480px) { .composer textarea, .card input { font-size: 16px; } }
180
+ .sr { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
181
+ `;var inline=/(`[^`\n]+`)|(\*\*[^*\n]+\*\*)|(\*[^*\s][^*\n]*\*|_[^_\s][^_\n]*_)|((?:https?:\/\/|www\.)[^\s<>"]*[^\s<>".,:;'!?)\]])/g;function link(href,text){let a=document.createElement("a");return a.href=href,a.textContent=text,a.target="_blank",a.rel="noopener noreferrer nofollow ugc",a}function wrap(tag,children){let element=document.createElement(tag);return element.append(...children),element}function inlineNodes(text){let nodes=[],last=0;for(let match of text.matchAll(inline)){let index=match.index??0;if(index>last)nodes.push(document.createTextNode(text.slice(last,index)));let[whole,code,bold,italic,url]=match;if(code)nodes.push(wrap("code",[document.createTextNode(code.slice(1,-1))]));else if(bold)nodes.push(wrap("strong",inlineNodes(bold.slice(2,-2))));else if(italic)nodes.push(wrap("em",inlineNodes(italic.slice(1,-1))));else if(url){let href=url.startsWith("www.")?`https://${url}`:url;try{let parsed=new URL(href);nodes.push(parsed.protocol==="http:"||parsed.protocol==="https:"?link(parsed.href,url):document.createTextNode(url))}catch{nodes.push(document.createTextNode(url))}}last=index+whole.length}if(last<text.length)nodes.push(document.createTextNode(text.slice(last)));return nodes}function richText(body){let fragment=document.createDocumentFragment(),parts=body.trim().replace(/\r\n?/g,`
182
+ `).split(/^```[^\n]*\n([\s\S]*?)^```[ \t]*$/m);return parts.forEach((part,i)=>{if(i%2===1){let pre=document.createElement("pre");pre.textContent=part.replace(/\n$/,""),fragment.append(pre);return}(parts.length>1?part.replace(/^\n|\n$/g,""):part).split(`
183
+ `).forEach((line,n)=>{if(n>0)fragment.append(document.createElement("br"));fragment.append(...inlineNodes(line))})}),fragment}var catalogs={en:messages,tr:messages2},maxFiles=10,groupGap=300000,icons={close:'<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg>',back:'<svg class="flip" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 6l-6 6 6 6"/></svg>',attach:'<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 11.5l-8.3 8.3a5 5 0 01-7-7L13 4.5a3.3 3.3 0 014.7 4.7l-8.3 8.3a1.7 1.7 0 01-2.4-2.4L14.5 7.6"/></svg>',send:'<svg class="flip" width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M3.4 20.4l17.5-7.5a1 1 0 000-1.8L3.4 3.6a1 1 0 00-1.4 1.2L4.5 12l-2.5 7.2a1 1 0 001.4 1.2zM4.5 12h7"/></svg>',file:'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" aria-hidden="true"><path d="M14 3H6v18h12V7z M14 3v4h4"/></svg>'};function el(tag,className,text){let element=document.createElement(tag);if(className)element.className=className;if(text!==void 0)element.textContent=text;return element}function iconButton(icon,label,onClick,className="icon"){let button=el("button",className);return button.type="button",button.innerHTML=icon,button.setAttribute("aria-label",label),button.title=label,button.addEventListener("click",onClick),button}var time=(value)=>Date.parse(value),byCreated=(a,b)=>time(a.created_at)-time(b.created_at),byActivity=(a,b)=>time(b.last_message_at??b.created_at)-time(a.last_message_at??a.created_at),authorKey=(m)=>m.author.type==="contact"?"contact":`${m.author.type}:${m.author.name??""}`,isImage=(a)=>/^image\/(png|jpeg|gif|webp)$/.test(a.content_type);class Chat{#host;#i18n;#client;#inbox=null;#contact=null;#phase="loading";#error="";#starting=null;#beginning=null;#conversations=new Map;#conversationCursor;#threads=new Map;#pending=[];#typing=new Map;#view=null;#reconnecting=!1;#files=[];#typingSentAt=0;#typingIdle=null;#blobs=new Map;#blobLoads=new Map;#avatars=new Map;#emailSaved=!1;#emailError="";#connectedBefore=!1;#header=el("header","header");#banner=el("div","banner");#body=el("div","body");#scroll=el("div","scroll");#email=el("div","card");#emailInput=el("input");#composer=el("div","composer");#chips=el("div","chips");#textarea=el("textarea");#fileInput=el("input");#sendButton;#attachButton;#live=el("div","sr");#scrollThread;#atEnd=!0;constructor(host){this.#host=host;let{server,channel}=host.config();this.#client=createYuvaClient2({server,channel:channel??"",identityToken:()=>host.identityToken()}),this.#client.on("event",(message)=>this.#event(message)),this.#client.on("connection",({state,attempt})=>{if(state!=="reconnecting")return;this.#reconnecting=attempt>0,this.#renderBanner()}),this.#client.on("session",({session,renewed})=>{if(this.#setInbox(session.inbox),this.#contact=session.contact,renewed)this.#reload()}),this.#setLocale(),this.#live.setAttribute("aria-live","polite"),this.#scroll.addEventListener("scroll",()=>{if(this.#atEnd=this.#scroll.scrollHeight-this.#scroll.scrollTop-this.#scroll.clientHeight<48,this.#scroll.scrollTop<60&&this.#view?.kind==="thread"&&this.#view.id)this.#loadMessages(this.#view.id,!0)}),this.#textarea.rows=1,this.#textarea.addEventListener("keydown",(event)=>{if(event.key==="Enter"&&!event.shiftKey&&!event.isComposing)event.preventDefault(),this.#send()}),this.#textarea.addEventListener("input",()=>{this.#autosize(),this.#syncSend(),this.#typingInput()}),this.#textarea.addEventListener("paste",(event)=>{let files=Array.from(event.clipboardData?.files??[]);if(files.length===0)return;event.preventDefault(),this.#addFiles(files)}),this.#fileInput.type="file",this.#fileInput.multiple=!0,this.#fileInput.hidden=!0,this.#fileInput.addEventListener("change",()=>{this.#addFiles(Array.from(this.#fileInput.files??[])),this.#fileInput.value=""}),this.#attachButton=iconButton(icons.attach,"",()=>this.#fileInput.click()),this.#sendButton=iconButton(icons.send,"",()=>void this.#send(),"icon send");let form=el("form");form.addEventListener("submit",(event)=>{event.preventDefault(),this.#send()}),form.append(this.#attachButton,this.#textarea,this.#sendButton,this.#fileInput),this.#composer.append(this.#chips,form),this.#emailInput.type="email",this.#emailInput.required=!0,this.#emailInput.autocomplete="email",document.addEventListener("visibilitychange",this.#onVisible),window.addEventListener("online",this.#onVisible),this.#renderAll(),this.#start()}update(){this.#setLocale(),this.#renderAll()}opened(){if(this.#phase==="error"){this.#starting=null,this.#start();return}if(this.#phase==="ready")this.#client.token?this.#refreshSession():this.#preview().then(()=>this.#render()).catch(()=>{return});if(this.#render(),this.#scrollToEnd(),this.#markRead(),this.#host.config().layout==="launcher")this.#textarea.focus({preventScroll:!0})}closed(){this.#stopTyping()}identityChanged(){this.#reset(),this.#start()}async signOut(){let signingOut=this.#client.signOut();if(this.#reset(),await signingOut,this.#host.isOpen())this.#start();else this.#render()}destroy(){this.#client.reset(),this.#stopTyping(),document.removeEventListener("visibilitychange",this.#onVisible),window.removeEventListener("online",this.#onVisible);for(let url of this.#blobs.values())URL.revokeObjectURL(url)}#reset(){this.#client.reset(),this.#starting=null,this.#beginning=null,this.#inbox=null,this.#contact=null,this.#phase="loading",this.#conversations.clear(),this.#threads.clear(),this.#pending=[],this.#view=null,this.#connectedBefore=!1,this.#host.setUnread(0)}#onVisible=()=>{if(document.visibilityState!=="visible")return;this.#client.reconnect(),this.#markRead(),this.#refreshInbox()};#setLocale(){let{locale}=this.#host.config();this.#i18n=setupI18n({locale,messages:{[locale]:catalogs[locale]??messages}})}#t(descriptor){return this.#i18n._(descriptor)}#start(){return this.#starting??=(async()=>{this.#phase="loading",this.#render();let{channel}=this.#host.config();if(!channel)throw new ApiError2(0,"not_configured");if(await this.#host.identityToken()||readStored(channel).session)await this.#begin();else if(await this.#preview(),!this.#inbox?.chat.allow_anonymous)throw new ApiError2(403,"anonymous_not_allowed");if(this.#phase="ready",!this.#view){let list=this.#sorted();if(this.#view=list.length>1?{kind:"list"}:{kind:"thread",id:list[0]?.id??null},this.#view.kind==="thread"&&this.#view.id)await this.#loadMessages(this.#view.id)}this.#render(),this.#scrollToEnd(),this.#markRead()})().catch((error)=>{if(this.#starting=null,this.#refusal(error))return;this.#phase="error",this.#error=error instanceof ApiError2?error.code:"error",this.#render()}),this.#starting}async#preview(){let inbox=await this.#fetchInbox();if(!this.#client.token)this.#setInbox(inbox)}#fetchInbox(){if(!this.#host.config().channel)throw new ApiError2(0,"not_configured");return this.#client.channelSettings()}#setInbox(inbox){let{channel}=this.#host.config();this.#inbox=inbox;let launcher=launcherOf(inbox);if(this.#host.setLauncher(launcher),channel)writeStored(channel,{...readStored(channel),launcher})}async#refreshInbox(){if(this.#phase!=="ready")return;try{this.#setInbox(await this.#fetchInbox()),this.#render()}catch(error){this.#refusal(error,!0)}}#refusal(error,channelRequest=!this.#client.token){let refused=error instanceof ApiError2&&(error.code==="origin_not_allowed"||error.status===404&&channelRequest);if(refused)this.#host.refused();return refused}#begin(){return this.#beginning??=(async()=>{await this.#client.start(),await this.#loadConversations(),await this.#client.connect()})().finally(()=>{this.#beginning=null}),this.#beginning}async#refreshSession(){try{await this.#client.refresh(),this.#render()}catch{}}async#loadConversations(more=!1){let page=await this.#client.listConversations({limit:50,cursor:more?this.#conversationCursor:void 0});if(!more)this.#conversations.clear();for(let conversation of page.items)this.#conversations.set(conversation.id,conversation);this.#conversationCursor=page.next_cursor,this.#syncUnread()}async#reload(){await this.#loadConversations();let current=this.#view?.kind==="thread"?this.#view.id:null;for(let id of[...this.#threads.keys()])if(id!==current)this.#threads.delete(id);if(current)this.#threads.delete(current),await this.#loadMessages(current);this.#render()}async#loadMessages(id,older=!1){let thread=this.#threads.get(id);if(!thread)thread={messages:[],complete:!1,loading:!1},this.#threads.set(id,thread);if(thread.loading||older&&(thread.complete||!thread.cursor))return;thread.loading=!0;let before=this.#scroll.scrollHeight-this.#scroll.scrollTop;try{let page=await this.#client.listMessages(id,{order:"desc",limit:30,cursor:older?thread.cursor:void 0}),known=new Set(thread.messages.map((m)=>m.id));thread.messages=[...thread.messages,...page.items.filter((m)=>!known.has(m.id))].sort(byCreated),thread.cursor=page.next_cursor,thread.complete=!page.next_cursor}finally{thread.loading=!1}if(older)this.#render(),this.#scroll.scrollTop=this.#scroll.scrollHeight-before}#event(message){switch(message.type){case"ready":if(this.#reconnecting=!1,this.#renderBanner(),this.#connectedBefore)this.#refreshInbox();this.#connectedBefore=!0;return;case"inbox.updated":this.#setInbox(message.data);break;case"resync_required":this.#reload();return;case"presence":if(this.#inbox)this.#inbox={...this.#inbox,presence:message.data};break;case"conversation.created":if(!this.#conversations.has(message.data.id))this.#conversations.set(message.data.id,message.data);if(this.#view?.kind==="thread"&&this.#view.id===null&&this.#conversations.size>1)this.#view=this.#pending.some((p)=>p.conversation===null)?this.#view:{kind:"list"};this.#syncUnread();break;case"conversation.updated":{let conversation=this.#conversations.get(message.data.id);if(conversation)this.#conversations.set(conversation.id,{...conversation,status:message.data.status});break}case"message.created":case"message.updated":this.#upsertMessage(message.data,message.type==="message.created");break;case"read":{let conversation=this.#conversations.get(message.data.conversation_id),previous=conversation?.last_read_by_member_at;if(conversation&&(!previous||time(message.data.read_at)>time(previous)))this.#conversations.set(conversation.id,{...conversation,last_read_by_member_at:message.data.read_at});break}case"typing":{let id=message.data.conversation_id,previous=this.#typing.get(id);if(previous)clearTimeout(previous.timer);if(message.data.typing){let name=message.data.author.name??"";this.#typing.set(id,{name,timer:setTimeout(()=>this.#typing.delete(id)&&this.#render(),8000)})}else this.#typing.delete(id);break}}this.#render()}#upsertMessage(message,created){let id=message.conversation_id,thread=this.#threads.get(id);if(thread){let index=thread.messages.findIndex((m)=>m.id===message.id);if(index>=0)thread.messages[index]=message;else thread.messages=[...thread.messages,message].sort(byCreated)}if(message.client_id)this.#pending=this.#pending.filter((p)=>p.client_id!==message.client_id);let conversation=this.#conversations.get(id);if(!conversation){this.#client.getConversation(id).then((c)=>{this.#conversations.set(c.id,c),this.#syncUnread(),this.#render()}).catch(()=>{return});return}if(created){let text=message.body.replace(/\s+/g," ").trim();if(this.#conversations.set(id,{...conversation,last_message:{id:message.id,author_type:message.author.type,text:text.length>140?`${text.slice(0,139)}…`:text,created_at:message.created_at},last_message_at:message.created_at,unread:conversation.unread||message.author.type!=="contact"}),message.author.type!=="contact"){let typing=this.#typing.get(id);if(typing)clearTimeout(typing.timer);this.#typing.delete(id),this.#announce(message)}}this.#syncUnread(),this.#markRead()}#announce(message){this.#live.textContent=`${message.author.name||this.#inbox?.name||""}: ${message.body.slice(0,200)}`}#syncUnread(){let count=0;for(let conversation of this.#conversations.values())if(conversation.unread)count++;this.#host.setUnread(count)}#visibleThread(){if(!this.#host.isOpen()||document.visibilityState!=="visible")return null;return this.#view?.kind==="thread"?this.#view.id:null}#markRead(){let id=this.#visibleThread(),conversation=id?this.#conversations.get(id):void 0;if(!id||!conversation?.unread)return;this.#conversations.set(id,{...conversation,unread:!1}),this.#syncUnread(),this.#client.markRead(id).catch(()=>{let current=this.#conversations.get(id);if(current)this.#conversations.set(id,{...current,unread:!0});this.#syncUnread()})}#addFiles(files){this.#files=[...this.#files,...files].slice(0,maxFiles),this.#renderChips(),this.#syncSend(),this.#textarea.focus()}#autosize(){if(this.#textarea.style.blockSize="",this.#textarea.value==="")return;this.#textarea.style.blockSize="auto",this.#textarea.style.blockSize=`${Math.max(38,Math.min(this.#textarea.scrollHeight,140))}px`}#syncSend(){this.#sendButton.disabled=this.#textarea.value.trim()===""&&this.#files.length===0}#typingInput(){let id=this.#view?.kind==="thread"?this.#view.id:null;if(!id||this.#textarea.value.trim()===""){this.#stopTyping();return}if(Date.now()-this.#typingSentAt>3000)this.#typingSentAt=Date.now(),this.#client.setTyping(id,!0).catch(()=>{return});if(this.#typingIdle)clearTimeout(this.#typingIdle);this.#typingIdle=setTimeout(()=>this.#stopTyping(),4000)}#stopTyping(){if(this.#typingIdle)clearTimeout(this.#typingIdle);if(this.#typingIdle=null,this.#typingSentAt===0)return;this.#typingSentAt=0;let id=this.#view?.kind==="thread"?this.#view.id:null;if(id)this.#client.setTyping(id,!1).catch(()=>{return})}async#send(){if(this.#phase!=="ready"||this.#view?.kind!=="thread")return;let body=this.#textarea.value.trim(),files=this.#files;if(!body&&files.length===0)return;if(this.#view.id===null&&this.#pending.some((p)=>p.conversation===null))return;this.#stopTyping(),this.#textarea.value="",this.#files=[],this.#autosize(),this.#renderChips(),this.#syncSend();let pending={client_id:randomId(),conversation:this.#view.id,body,files,state:"sending"};this.#pending.push(pending),this.#render(),this.#scrollToEnd(),await this.#deliver(pending)}async#deliver(pending){pending.state="sending",this.#render();let input={body:pending.body,files:pending.files,client_id:pending.client_id};try{if(pending.conversation===null){let created=await this.#client.startConversation(input),id=created.conversation.id;if(!this.#conversations.has(id))this.#conversations.set(id,created.conversation);if(!this.#threads.has(id))this.#threads.set(id,{messages:[],complete:!0,loading:!1});if(this.#view?.kind==="thread"&&this.#view.id===null)this.#view={kind:"thread",id};this.#upsertMessage(created.message,!1)}else{let message=await this.#client.sendMessage(pending.conversation,input);this.#upsertMessage(message,!0)}this.#pending=this.#pending.filter((p)=>p!==pending)}catch(error){let status=error instanceof ApiError2?error.status:0;pending.state=status===413||status===415?"refused":"failed",pending.status=status}this.#render(),this.#scrollToEnd()}async#saveEmail(){let email=this.#emailInput.value.trim();if(!email||!this.#emailInput.checkValidity()){this.#emailError=this.#t({id:"E_qvzP"}),this.#renderEmail();return}try{this.#contact=await this.#client.setEmail(email),this.#emailSaved=!0,this.#emailError=""}catch{this.#emailError=this.#t({id:"ZxJcgC"})}this.#renderEmail()}#attachmentUrl(attachment){let cached=this.#blobs.get(attachment.id);if(cached)return Promise.resolve(cached);let load=this.#blobLoads.get(attachment.id);if(!load)load=this.#client.attachment(attachment.id).then((blob)=>{let url=URL.createObjectURL(blob);return this.#blobs.set(attachment.id,url),url}),load.catch(()=>this.#blobLoads.delete(attachment.id)),this.#blobLoads.set(attachment.id,load);return load}async#download(attachment){let url=await this.#attachmentUrl(attachment),a=el("a");a.href=url,a.download=attachment.filename,a.rel="noopener",a.click()}#sorted(){return[...this.#conversations.values()].sort(byActivity)}#nobodyAvailable(){let inbox=this.#inbox;if(!inbox)return!1;return!inbox.open_now||inbox.mode==="async"||!inbox.presence?.available}#scrollToEnd(){this.#atEnd=!0,this.#scroll.scrollTop=this.#scroll.scrollHeight,requestAnimationFrame(()=>{if(this.#atEnd)this.#scroll.scrollTop=this.#scroll.scrollHeight})}#renderAll(){this.#textarea.placeholder=this.#t({id:"RzWUUK"}),this.#textarea.setAttribute("aria-label",this.#t({id:"xDAtGP"})),this.#attachButton.setAttribute("aria-label",this.#t({id:"N7m8E-"})),this.#attachButton.title=this.#t({id:"N7m8E-"}),this.#sendButton.setAttribute("aria-label",this.#t({id:"JlFcis"})),this.#sendButton.title=this.#t({id:"JlFcis"}),this.#emailInput.placeholder=this.#t({id:"njBr8W"}),this.#emailInput.setAttribute("aria-label",this.#t({id:"XJURKP"})),this.#syncSend(),this.#renderChips(),this.#render()}#render(){let panel=this.#host.panel;if(panel.childElementCount===0){let style=el("style");style.textContent=styles,panel.append(style,this.#header,this.#banner,this.#body,this.#live),this.#header.id="yuva-title",panel.setAttribute("aria-labelledby","yuva-title")}if(this.#renderHeader(),this.#renderBanner(),this.#phase!=="ready"){this.#scrollThread=void 0;let center=el("div","center");if(this.#phase==="loading")center.append(el("p","hint",this.#t({id:"Pwqkdw"})));else{center.append(el("p","empty",this.#errorText()));let retry=el("button","primary",this.#t({id:"KDw4GX"}));retry.type="button",retry.addEventListener("click",()=>{this.#starting=null,this.#start()}),center.append(retry)}this.#body.replaceChildren(center);return}if(this.#view?.kind==="list")this.#renderList();else this.#renderThread()}#errorText(){switch(this.#error){case"anonymous_not_allowed":return this.#t({id:"zLzEI3"});case"invalid_identity_token":return this.#t({id:"0XGLbP"});case"rate_limited":return this.#t({id:"tzsBOP"});case"network":return this.#t({id:"GQVIIl"});default:return this.#t({id:"Amai22"})}}#renderHeader(){let{layout}=this.#host.config(),inbox=this.#inbox,children=[],view=this.#view;if(this.#phase==="ready"&&view?.kind==="thread"&&(this.#conversations.size>1||view.id===null&&this.#conversations.size>0))children.push(iconButton(icons.back,this.#t({id:"S63MQu"}),()=>{this.#stopTyping(),this.#view={kind:"list"},this.#render()}));let heading=el("div","heading");if(heading.append(el("h2","title",inbox?.name||this.#t({id:"t7TeQU"}))),inbox){let status=el("div","status");if(inbox.mode==="live"&&inbox.open_now&&inbox.presence?.available){let avatars=el("div","avatars");for(let member of inbox.presence?.members.slice(0,3)??[]){let avatar=el("span","avatar",member.initials||"•");avatar.title=member.name,avatars.append(avatar)}status.append(avatars,el("span","dot"),el("span","",this.#t({id:"oXOSPE"})))}else if(!inbox.open_now||inbox.mode==="live")status.append(el("span","dot away"),el("span","",this.#t({id:"1EICzz"})));else status.append(el("span","",this.#replyTime(inbox.expected_reply_minutes)));heading.append(status)}if(children.push(heading),layout==="launcher")children.push(iconButton(icons.close,this.#t({id:"yz7wBu"}),()=>this.#host.close()));this.#header.replaceChildren(...children)}#replyTime(minutes){if(!minutes)return this.#t({id:"nhTADq"});let{locale}=this.#host.config(),[value,unit]=minutes<60?[minutes,"minute"]:minutes<1440?[Math.round(minutes/60),"hour"]:[Math.round(minutes/1440),"day"],duration=new Intl.NumberFormat(locale,{style:"unit",unit,unitDisplay:"long"}).format(value);return this.#t({id:"VggdQJ",values:{duration}})}#renderBanner(){this.#banner.hidden=!this.#reconnecting,this.#banner.textContent=this.#t({id:"09Tv5j"})}#renderList(){this.#scrollThread=void 0;let{locale}=this.#host.config(),list=el("ul","list"),today=new Date().toDateString();for(let conversation of this.#sorted()){let button=el("button",conversation.unread?"item unread":"item");button.type="button";let main=el("div","item-main"),top=el("div","item-top");top.append(el("span","item-subject",conversation.subject||this.#inbox?.name||this.#t({id:"YyNOE7"})));let at=new Date(conversation.last_message_at??conversation.created_at);top.append(el("span","item-time",new Intl.DateTimeFormat(locale,at.toDateString()===today?{timeStyle:"short"}:{month:"short",day:"numeric"}).format(at)));let preview=conversation.last_message?`${conversation.last_message.author_type==="contact"?this.#t({id:"ydzYKJ"})+" ":""}${conversation.last_message.text||this.#t({id:"UY1vmE"})}`:"",previewLine=el("div","item-preview",preview.replace(/\*\*|`/g,""));if(previewLine.dir="auto",main.append(top,previewLine),button.append(main),conversation.unread)button.append(el("span","unread-dot"));button.addEventListener("click",()=>void this.#openThread(conversation.id));let item=el("li");item.append(button),list.append(item)}if(this.#conversationCursor){let more=el("button","link",this.#t({id:"ASr-r0"}));more.type="button",more.style.margin="8px auto",more.style.display="block",more.addEventListener("click",()=>void this.#loadConversations(!0).then(()=>this.#render())),list.append(more)}let start=el("button","primary",this.#t({id:"-HY5kX"}));start.type="button",start.addEventListener("click",()=>{this.#view={kind:"thread",id:null},this.#render(),this.#textarea.focus()}),this.#body.replaceChildren(list,start)}async#openThread(id){if(this.#view={kind:"thread",id},this.#render(),!this.#threads.get(id)?.complete&&!this.#threads.get(id)?.messages.length)await this.#loadMessages(id).catch(()=>{return});this.#render(),this.#scrollToEnd(),this.#markRead()}#avatar(author){let avatar=el("span","avatar",author.initials||""),url=author.avatar_url,state=url?this.#avatars.get(url):!1;if(!url||state===!1)return avatar;let img=el("img");if(img.alt="",img.referrerPolicy="no-referrer",state)return img.src=url,avatar.replaceChildren(img),avatar;return img.addEventListener("load",()=>{if(this.#avatars.set(url,!0),avatar.isConnected)avatar.replaceChildren(img)}),img.addEventListener("error",()=>this.#avatars.set(url,!1)),img.src=url,avatar}#renderThread(){let view=this.#view;if(view?.kind!=="thread")return;let id=view.id,sameThread=this.#scrollThread===id,atEnd=this.#atEnd,thread=id?this.#threads.get(id):void 0,messages=thread?.messages??[],pending=this.#pending.filter((p)=>p.conversation===id),nodes=[],greeting=this.#inbox?.chat.greeting;if(!thread||thread.complete){if(greeting){let bubble=el("div","bubble greeting");bubble.append(richText(greeting)),nodes.push(bubble)}else if(messages.length===0&&pending.length===0){let center=el("div","center");center.append(el("p","empty",this.#t({id:"meAa31"})),el("p","hint",this.#t({id:"SdV5Cn"}))),nodes.push(center)}}else if(thread.loading)nodes.push(el("p","hint",this.#t({id:"Pwqkdw"})));messages.forEach((message,i)=>{let previous=messages[i-1],next=messages[i+1]??(pending.length>0?null:void 0),mine=message.author.type==="contact",starts=!previous||authorKey(previous)!==authorKey(message)||time(message.created_at)-time(previous.created_at)>groupGap,ends=next===void 0||(next===null?!mine:authorKey(next)!==authorKey(message)||time(next.created_at)-time(message.created_at)>groupGap);if(!mine&&starts&&message.author.name)nodes.push(el("div","author",message.author.name));let row=el("div",`row ${mine?"mine":"theirs"}${starts&&i>0?" group-gap":""}`);if(!mine){let avatar=this.#avatar(message.author);if(!ends)avatar.style.visibility="hidden";row.append(avatar)}let bubble=el("div","bubble");if(bubble.dir="auto",bubble.title=new Date(message.created_at).toLocaleString(this.#host.config().locale),message.body.trim())bubble.append(richText(message.body));if(message.attachments.length>0)bubble.append(this.#renderAttachments(message.attachments));if(row.append(bubble),nodes.push(row),ends){let meta=this.#formatTime(message.created_at);if(mine&&i===messages.length-1&&pending.length===0){let readAt=id?this.#conversations.get(id)?.last_read_by_member_at:void 0,seen=readAt&&time(readAt)>=time(message.created_at);meta+=` · ${seen?this.#t({id:"6lGaum"}):this.#t({id:"h69WC6"})}`}let line=el("div","meta",meta);if(!mine)line.style.alignSelf="flex-start";if(!mine)line.style.marginInlineStart="36px";nodes.push(line)}}),pending.forEach((item,i)=>{let row=el("div",`row mine${i===0&&messages.length>0&&messages[messages.length-1]?.author.type!=="contact"?" group-gap":""}`),bubble=el("div","bubble");if(bubble.dir="auto",item.body)bubble.append(richText(item.body));if(item.files.length>0){let files=el("div","files");for(let file of item.files)files.append(el("span","file",file.name||"image"));bubble.append(files)}if(row.append(bubble),nodes.push(row),item.state==="failed"){let line=el("div","meta failed",`${this.#t({id:"upFvcp"})} `),retry=el("button","link",this.#t({id:"6gRgw8"}));retry.type="button",retry.addEventListener("click",()=>void this.#deliver(item)),line.append(retry),nodes.push(line)}else if(item.state==="refused"){let reason=item.status===413?this.#t({id:"eOu9Px"}):this.#t({id:"JkW3Ph"});nodes.push(el("div","meta failed",`${this.#t({id:"upFvcp"})} ${reason}`))}else if(i===pending.length-1)nodes.push(el("div","meta",this.#t({id:"Sad2tK"})))});let typing=id?this.#typing.get(id):void 0;if(typing){let line=el("div","typing");line.append(el("i"),el("i"),el("i"));let name=typing.name;line.append(el("span","",name?this.#t({id:"tHaDPf",values:{name}}):this.#t({id:"PxYAul"}))),nodes.push(line)}if(this.#scroll.replaceChildren(...nodes),this.#renderEmail(),this.#body.firstChild!==this.#scroll)this.#body.replaceChildren(this.#scroll,this.#email,this.#composer);if(this.#scrollThread=id,!sameThread||atEnd)this.#scrollToEnd()}#renderAttachments(attachments){let files=el("div","files");for(let attachment of attachments)if(isImage(attachment)){let img=el("img");img.alt=attachment.filename;let cached=this.#blobs.get(attachment.id);if(cached)img.src=cached;else this.#attachmentUrl(attachment).then((url)=>{img.src=url}).catch(()=>{return});img.addEventListener("load",()=>{if(this.#atEnd)this.#scroll.scrollTop=this.#scroll.scrollHeight}),img.addEventListener("click",()=>void this.#download(attachment)),files.append(img)}else{let button=el("button","file");button.type="button",button.innerHTML=icons.file,button.append(el("span","",attachment.filename)),button.addEventListener("click",()=>void this.#download(attachment)),files.append(button)}return files}#formatTime(value){return new Intl.DateTimeFormat(this.#host.config().locale,{timeStyle:"short"}).format(new Date(value))}#renderEmail(){let contact=this.#contact,chat=this.#inbox?.chat,sent=(this.#view?.kind==="thread"?this.#view.id:null)!==null||this.#pending.length>0,known=contact?.email||contact?.typed_email,show=!!chat?.ask_email_offline&&this.#nobodyAvailable()&&sent&&(!known||this.#emailSaved);if(this.#email.hidden=!show,!show)return;if(known&&this.#emailSaved){this.#email.replaceChildren(el("p","",this.#t({id:"iGNUZJ",values:{known}})));return}if(this.#email.querySelector("form")){let error=this.#email.querySelector(".error");if(error)error.textContent=this.#emailError;else if(this.#emailError)this.#email.append(el("p","error",this.#emailError));return}let form=el("form");form.noValidate=!0,form.addEventListener("submit",(event)=>{event.preventDefault(),this.#saveEmail()});let save=el("button","",this.#t({id:"tfDRzk"}));if(save.type="submit",form.append(this.#emailInput,save),this.#email.replaceChildren(el("p","",this.#t({id:"M62PBt"})),form),this.#emailError)this.#email.append(el("p","error",this.#emailError))}#renderChips(){this.#chips.hidden=this.#files.length===0,this.#chips.replaceChildren(...this.#files.map((file,index)=>{let chip=el("span","chip");return chip.append(el("span","",file.name||"image"),iconButton(icons.close,this.#t({id:"w61W3L",values:{0:file.name}}),()=>{this.#files=this.#files.filter((_,i)=>i!==index),this.#renderChips(),this.#syncSend()})),chip}))}}function mount(host){return new Chat(host)}export{mount};
@@ -0,0 +1,9 @@
1
+ import { type Client, type ClientOptions } from "openapi-fetch";
2
+ import type { components, operations, paths } from "./schema.gen.js";
3
+ export type { components, operations, paths };
4
+ export type YuvaApi = Client<paths>;
5
+ export interface YuvaApiOptions extends Omit<ClientOptions, "baseUrl"> {
6
+ server: string;
7
+ apiKey: string;
8
+ }
9
+ export declare function createYuvaApi({ server, apiKey, headers, ...options }: YuvaApiOptions): YuvaApi;
@@ -0,0 +1,3 @@
1
+ import { define, YuvaChatElement } from "./element.js";
2
+ export { define, YuvaChatElement };
3
+ export type { IdentityTokenSource, Layout } from "./types.js";