@heedb/web-sdk 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,18 +1,30 @@
1
1
  # @heedb/web-sdk
2
2
 
3
- Drop-in feedback widget with email conversations for any website.
3
+ A lightweight (~12 KB) feedback widget that drops into any website. Customers can send messages, make privacy requests, and view their conversation history — all through email-based threads. No login required.
4
4
 
5
- ## CDN (quickest)
5
+ [Dashboard](https://heedb.com) · [GitHub](https://github.com/TheAleSch/heedb)
6
+
7
+ ---
8
+
9
+ ## Quick start
10
+
11
+ ### CDN (recommended)
12
+
13
+ The fastest way — no build step, no dependencies. Add one line before `</body>`:
6
14
 
7
15
  ```html
8
16
  <script
9
- src="https://cdn.jsdelivr.net/npm/@heedb/web-sdk/dist/widget.js"
17
+ src="https://cdn.jsdelivr.net/npm/@heedb/web-sdk/widget.js"
10
18
  data-api-key="YOUR_API_KEY"
11
19
  data-host="https://heedb.com"
12
20
  ></script>
13
21
  ```
14
22
 
15
- ## npm
23
+ > **Important:** `data-host` is required when loading from a CDN. It tells the widget where to send API requests. Omit it only when the script is served from the same domain as your Heedb instance.
24
+
25
+ A floating chat button appears in the bottom-right corner. That's it.
26
+
27
+ ### npm
16
28
 
17
29
  ```bash
18
30
  npm install @heedb/web-sdk
@@ -21,29 +33,452 @@ npm install @heedb/web-sdk
21
33
  ```ts
22
34
  import { Heedb } from "@heedb/web-sdk";
23
35
 
36
+ Heedb.init({ apiKey: "YOUR_API_KEY" });
37
+ ```
38
+
39
+ This dynamically loads the widget and attaches it to the page. Works with React, Next.js, Vue, Svelte, or any framework.
40
+
41
+ ### Self-hosted
42
+
43
+ If you run your own Heedb instance, serve `widget.js` from your domain:
44
+
45
+ ```html
46
+ <script src="https://your-instance.com/widget.js" data-api-key="YOUR_API_KEY"></script>
47
+ ```
48
+
49
+ The widget auto-detects the API host from the script's origin — no `data-host` needed.
50
+
51
+ ---
52
+
53
+ ## Environment variables
54
+
55
+ Add these to your `.env` (or equivalent):
56
+
57
+ ```env
58
+ # Public — safe for client-side code, used in data-api-key or Heedb.init()
59
+ NEXT_PUBLIC_HEEDB_API_KEY=your_api_key_here
60
+
61
+ # Private — server-side only, used to generate userHash
62
+ # NEVER expose this in client-side code, bundle, or git
63
+ HEEDB_WIDGET_SECRET=your_widget_secret_here
64
+ ```
65
+
66
+ Both values are in your [dashboard settings](https://heedb.com/app/settings).
67
+
68
+ ---
69
+
70
+ ## Identify users
71
+
72
+ By default, the widget shows a form asking for name, email, and message. If the user is already logged in to your app, you can skip that step.
73
+
74
+ There are three levels:
75
+
76
+ ### 1. Anonymous (default)
77
+
78
+ No `init()` call needed. The widget shows the full contact form. Good for marketing sites, landing pages, or anywhere users aren't logged in.
79
+
80
+ ### 2. Identified (name + email)
81
+
82
+ Pre-fills the form and hides the name/email fields. The user only sees the message box. **No server-side code required.**
83
+
84
+ ```ts
24
85
  Heedb.init({
25
86
  apiKey: "YOUR_API_KEY",
26
- email: user.email, // optional
27
- name: user.name, // optional
28
- userHash: serverHash, // optional — HMAC-SHA256 for verified identity
87
+ email: "jane@example.com",
88
+ name: "Jane",
29
89
  });
30
90
  ```
31
91
 
32
- ## Identify users
92
+ Use this when you know the user's identity but don't need to show them their conversation history.
93
+
94
+ ### 3. Verified (name + email + userHash)
95
+
96
+ Same as identified, plus unlocks the **Messages** tab where the user can view their previous conversation threads.
97
+
98
+ **This requires a server-generated HMAC.** The `userHash` must be computed on your backend and passed to the frontend — never generate it client-side, as that would expose your Widget Secret.
33
99
 
34
- Generate a `userHash` on your server to verify user identity:
100
+ ```ts
101
+ // Client-side — after receiving userHash from your server
102
+ Heedb.init({
103
+ apiKey: "YOUR_API_KEY",
104
+ email: "jane@example.com",
105
+ name: "Jane",
106
+ userHash: serverGeneratedHash, // from your backend
107
+ });
108
+ ```
109
+
110
+ **Only pass `userHash` when a user is authenticated.** For logged-out users, either call `init()` without it (identified) or don't call `init()` at all (anonymous).
111
+
112
+ ---
113
+
114
+ ## Generate the userHash (server-side)
115
+
116
+ The `userHash` is an HMAC-SHA256 of the user's email, signed with your **Widget Secret**. It proves your server vouches for this user's identity.
117
+
118
+ **The flow:**
119
+ 1. User logs in to your app
120
+ 2. Your server computes `HMAC-SHA256(email, WIDGET_SECRET)` → `userHash`
121
+ 3. Your server passes `userHash` to the frontend (via props, API response, or server-rendered HTML)
122
+ 4. The frontend calls `Heedb.init({ email, userHash })`
123
+
124
+ **Never compute the hash client-side — the Widget Secret must stay on your server.**
125
+
126
+ ### Node.js
35
127
 
36
128
  ```js
37
129
  const crypto = require("crypto");
38
- const userHash = crypto
39
- .createHmac("sha256", WIDGET_SECRET)
40
- .update(userEmail)
41
- .digest("hex");
130
+
131
+ function heedbUserHash(email) {
132
+ return crypto
133
+ .createHmac("sha256", process.env.HEEDB_WIDGET_SECRET)
134
+ .update(email)
135
+ .digest("hex");
136
+ }
137
+ ```
138
+
139
+ ### Python
140
+
141
+ ```python
142
+ import hmac, hashlib, os
143
+
144
+ def heedb_user_hash(email: str) -> str:
145
+ secret = os.environ["HEEDB_WIDGET_SECRET"].encode()
146
+ return hmac.new(secret, email.encode(), hashlib.sha256).hexdigest()
147
+ ```
148
+
149
+ ### PHP
150
+
151
+ ```php
152
+ function heedbUserHash(string $email): string {
153
+ return hash_hmac('sha256', $email, getenv('HEEDB_WIDGET_SECRET'));
154
+ }
155
+ ```
156
+
157
+ ### Ruby
158
+
159
+ ```ruby
160
+ require "openssl"
161
+
162
+ def heedb_user_hash(email)
163
+ OpenSSL::HMAC.hexdigest("sha256", ENV["HEEDB_WIDGET_SECRET"], email)
164
+ end
165
+ ```
166
+
167
+ ### Go
168
+
169
+ ```go
170
+ import (
171
+ "crypto/hmac"
172
+ "crypto/sha256"
173
+ "encoding/hex"
174
+ "os"
175
+ )
176
+
177
+ func heedbUserHash(email string) string {
178
+ h := hmac.New(sha256.New, []byte(os.Getenv("HEEDB_WIDGET_SECRET")))
179
+ h.Write([]byte(email))
180
+ return hex.EncodeToString(h.Sum(nil))
181
+ }
182
+ ```
183
+
184
+ ---
185
+
186
+ ## Framework examples
187
+
188
+ ### Next.js (App Router) — full example with verified identity
189
+
190
+ This is the recommended pattern. The hash is computed in a server component and passed to a client component.
191
+
192
+ ```tsx
193
+ // app/components/HeedbWidget.server.tsx — Server Component
194
+ import { auth } from "@/lib/auth"; // your auth library
195
+ import { headers } from "next/headers";
196
+ import { createHmac } from "crypto";
197
+ import HeedbWidgetClient from "./HeedbWidget.client";
198
+
199
+ export default async function HeedbWidget() {
200
+ let email: string | undefined;
201
+ let name: string | undefined;
202
+ let userHash: string | undefined;
203
+
204
+ try {
205
+ const session = await auth.api.getSession({ headers: await headers() });
206
+ if (session?.user?.email) {
207
+ email = session.user.email;
208
+ name = session.user.name ?? undefined;
209
+ // Compute HMAC on the server — secret never reaches the client
210
+ userHash = createHmac("sha256", process.env.HEEDB_WIDGET_SECRET!)
211
+ .update(email)
212
+ .digest("hex");
213
+ }
214
+ } catch {
215
+ // No session — widget will work anonymously
216
+ }
217
+
218
+ return <HeedbWidgetClient email={email} name={name} userHash={userHash} />;
219
+ }
220
+ ```
221
+
222
+ ```tsx
223
+ // app/components/HeedbWidget.client.tsx — Client Component
224
+ "use client";
225
+
226
+ import { useEffect } from "react";
227
+ import { Heedb } from "@heedb/web-sdk";
228
+
229
+ export default function HeedbWidgetClient({ email, name, userHash }: {
230
+ email?: string;
231
+ name?: string;
232
+ userHash?: string;
233
+ }) {
234
+ useEffect(() => {
235
+ Heedb.init({
236
+ apiKey: process.env.NEXT_PUBLIC_HEEDB_API_KEY!,
237
+ email,
238
+ name,
239
+ userHash,
240
+ });
241
+ }, [email, name, userHash]);
242
+
243
+ return null;
244
+ }
245
+ ```
246
+
247
+ ```tsx
248
+ // app/layout.tsx
249
+ import HeedbWidget from "./components/HeedbWidget.server";
250
+
251
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
252
+ return (
253
+ <html>
254
+ <body>
255
+ {children}
256
+ <HeedbWidget />
257
+ </body>
258
+ </html>
259
+ );
260
+ }
261
+ ```
262
+
263
+ ### Next.js (Pages Router)
264
+
265
+ Compute the hash in `getServerSideProps` and pass it as a prop:
266
+
267
+ ```tsx
268
+ // pages/_app.tsx
269
+ import { useEffect } from "react";
270
+ import { Heedb } from "@heedb/web-sdk";
271
+
272
+ export default function App({ Component, pageProps }: AppProps) {
273
+ useEffect(() => {
274
+ if (pageProps.heedbEmail) {
275
+ Heedb.init({
276
+ apiKey: process.env.NEXT_PUBLIC_HEEDB_API_KEY!,
277
+ email: pageProps.heedbEmail,
278
+ name: pageProps.heedbName,
279
+ userHash: pageProps.heedbUserHash,
280
+ });
281
+ } else {
282
+ Heedb.init({ apiKey: process.env.NEXT_PUBLIC_HEEDB_API_KEY! });
283
+ }
284
+ }, [pageProps.heedbEmail]);
285
+
286
+ return <Component {...pageProps} />;
287
+ }
288
+ ```
289
+
290
+ ### React (Vite / CRA) — with API route for hash
291
+
292
+ When you don't have server components, fetch the hash from an API endpoint:
293
+
294
+ ```ts
295
+ // Server: POST /api/heedb-hash
296
+ import crypto from "crypto";
297
+ export function handler(req, res) {
298
+ const { email } = req.body;
299
+ const hash = crypto
300
+ .createHmac("sha256", process.env.HEEDB_WIDGET_SECRET!)
301
+ .update(email)
302
+ .digest("hex");
303
+ res.json({ userHash: hash });
304
+ }
305
+ ```
306
+
307
+ ```tsx
308
+ // Client: App.tsx
309
+ import { useEffect } from "react";
310
+ import { Heedb } from "@heedb/web-sdk";
311
+
312
+ function App() {
313
+ const user = useAuth(); // your auth hook
314
+
315
+ useEffect(() => {
316
+ if (!user) {
317
+ Heedb.init({ apiKey: import.meta.env.VITE_HEEDB_API_KEY });
318
+ return;
319
+ }
320
+
321
+ // Fetch the hash from your server
322
+ fetch("/api/heedb-hash", {
323
+ method: "POST",
324
+ headers: { "Content-Type": "application/json" },
325
+ body: JSON.stringify({ email: user.email }),
326
+ })
327
+ .then((r) => r.json())
328
+ .then(({ userHash }) => {
329
+ Heedb.init({
330
+ apiKey: import.meta.env.VITE_HEEDB_API_KEY,
331
+ email: user.email,
332
+ name: user.name,
333
+ userHash,
334
+ });
335
+ });
336
+ }, [user]);
337
+
338
+ return <div>Your app</div>;
339
+ }
340
+ ```
341
+
342
+ ### Vue
343
+
344
+ ```vue
345
+ <script setup>
346
+ import { onMounted } from "vue";
347
+ import { Heedb } from "@heedb/web-sdk";
348
+
349
+ const props = defineProps<{
350
+ email?: string;
351
+ name?: string;
352
+ userHash?: string;
353
+ }>();
354
+
355
+ onMounted(() => {
356
+ Heedb.init({
357
+ apiKey: import.meta.env.VITE_HEEDB_API_KEY,
358
+ email: props.email,
359
+ name: props.name,
360
+ userHash: props.userHash,
361
+ });
362
+ });
363
+ </script>
42
364
  ```
43
365
 
44
- Then pass it to `Heedb.init({ email, userHash })`.
366
+ ### Svelte
367
+
368
+ ```svelte
369
+ <script>
370
+ import { onMount } from "svelte";
371
+ import { Heedb } from "@heedb/web-sdk";
372
+
373
+ export let email = undefined;
374
+ export let name = undefined;
375
+ export let userHash = undefined;
376
+
377
+ onMount(() => {
378
+ Heedb.init({
379
+ apiKey: import.meta.env.VITE_HEEDB_API_KEY,
380
+ email,
381
+ name,
382
+ userHash,
383
+ });
384
+ });
385
+ </script>
386
+ ```
387
+
388
+ ### Static HTML / WordPress / Webflow
389
+
390
+ Use the CDN script tag — no build tools needed:
391
+
392
+ ```html
393
+ <script
394
+ src="https://cdn.jsdelivr.net/npm/@heedb/web-sdk/widget.js"
395
+ data-api-key="YOUR_API_KEY"
396
+ data-host="https://heedb.com"
397
+ ></script>
398
+ ```
399
+
400
+ For verified identity on static sites, you'll need a small server endpoint that returns the `userHash` — see the React + API route example above.
401
+
402
+ ---
403
+
404
+ ## What the widget does
405
+
406
+ The widget adds a floating button to your page with three tabs:
407
+
408
+ | Tab | Description | Requires |
409
+ |-----|-------------|----------|
410
+ | **Message** | Contact form — name, email, and a message. Creates a new support thread. Fields are hidden when the user is identified. | Nothing |
411
+ | **Privacy** | GDPR/privacy request form — data export, deletion, or opt-out. | Nothing |
412
+ | **Messages** | Conversation history — shows open threads. | Verified identity (`userHash`) |
413
+
414
+ **The full conversation loop:**
415
+
416
+ 1. Customer submits a message via the widget
417
+ 2. A thread appears in your [Heedb dashboard](https://heedb.com/app/threads)
418
+ 3. You get an email notification
419
+ 4. You reply from the dashboard — the customer receives an email
420
+ 5. The customer replies to that email — it shows up in the dashboard
421
+ 6. If the customer is verified, they can also see the full conversation in the widget's Messages tab
422
+
423
+ ---
424
+
425
+ ## Configuration reference
426
+
427
+ ### Script tag attributes
428
+
429
+ | Attribute | Required | Description |
430
+ |-----------|----------|-------------|
431
+ | `data-api-key` | Yes | Your project's public API key |
432
+ | `data-host` | CDN: Yes. Self-hosted: No | API host URL. **Required when loading from CDN** (e.g. jsDelivr). When self-hosted, defaults to the script's origin. |
433
+
434
+ ### `Heedb.init()` options
435
+
436
+ | Option | Type | Required | Description |
437
+ |--------|------|----------|-------------|
438
+ | `apiKey` | `string` | npm: Yes. Script tag: No | Your project's public API key. Script tag reads it from `data-api-key`. |
439
+ | `host` | `string` | No | API host URL. Defaults to `https://heedb.com`. |
440
+ | `email` | `string` | No | User's email — pre-fills the form and hides the email field |
441
+ | `name` | `string` | No | User's name — pre-fills the form and hides the name field |
442
+ | `userHash` | `string` | No | Server-generated HMAC-SHA256 hash — unlocks conversation history. **Must come from your server.** |
443
+
444
+ ### Behavior by identity level
445
+
446
+ | What happens | Anonymous | Identified | Verified |
447
+ |---|---|---|---|
448
+ | `init()` called | No | Yes (email + name) | Yes (email + name + userHash) |
449
+ | Contact form visible | Yes (full form) | Yes (message only) | Yes (message only) |
450
+ | Privacy tab visible | Yes | Yes | Yes |
451
+ | Messages tab visible | No | No | Yes |
452
+ | Name/email fields | Shown | Hidden | Hidden |
453
+
454
+ ---
455
+
456
+ ## Security
457
+
458
+ | Credential | Where it lives | Purpose |
459
+ |------------|---------------|---------|
460
+ | **API Key** (`NEXT_PUBLIC_HEEDB_API_KEY`) | Client-side (public) | Identifies your project. Safe to embed in frontend code. |
461
+ | **Widget Secret** (`HEEDB_WIDGET_SECRET`) | Server-side only (private) | Signs the `userHash`. **Never expose in client code, bundles, or git.** |
462
+
463
+ - The API key controls which project receives the submission — it doesn't grant read access to any data
464
+ - The `userHash` is a cryptographic proof that your server vouches for the user's email — without it, users can submit messages but can't read thread history
465
+ - Restrict which domains can use your API key under **Settings > Allowed Domains** in the dashboard
466
+ - If no allowed domains are configured, the widget works from any origin (useful for development)
467
+
468
+ ---
469
+
470
+ ## Troubleshooting
471
+
472
+ | Problem | Cause | Fix |
473
+ |---------|-------|-----|
474
+ | Widget doesn't appear | Missing `data-api-key` or invalid API key | Check the key in your [dashboard settings](https://heedb.com/app/settings) |
475
+ | "Origin not allowed" (403) | Your domain isn't in the project's allowed domains list | Add your domain in Settings > Allowed Domains, or leave the list empty to allow all |
476
+ | "Failed to load messages" | `userHash` is missing, invalid, or computed with the wrong secret | Verify you're using the Widget Secret (not the API key) and computing HMAC-SHA256 server-side |
477
+ | Widget loads but `init()` has no effect | `init()` called before script loads | With npm: `Heedb.init()` handles this automatically. With CDN: call `init()` in the script's `onload` or after `DOMContentLoaded` |
478
+ | Messages tab not visible | User is identified but not verified | Pass `userHash` from your server to enable the Messages tab |
479
+
480
+ ---
45
481
 
46
- ## Links
482
+ ## License
47
483
 
48
- - [Dashboard](https://heedb.com)
49
- - [GitHub](https://github.com/TheAleSch/heedb)
484
+ MIT
package/dist/widget.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /* Heedb Widget — https://heedb.com */
2
- "use strict";(()=>{(function(){var $,q,A,O,R;let u=document.currentScript||document.querySelector("script[data-api-key]"),w=($=u==null?void 0:u.getAttribute("data-api-key"))!=null?$:"",_=(q=u==null?void 0:u.src)!=null?q:"",H=(u==null?void 0:u.getAttribute("data-host"))||(_?new URL(_).origin:"");if(!w){console.warn("[Heedb] Missing data-api-key on <script> tag.");return}let k=!1,x="contact",v=(A=localStorage.getItem("heedb_name"))!=null?A:"",r=(O=localStorage.getItem("heedb_email"))!=null?O:"",d=(R=localStorage.getItem("heedb_token"))!=null?R:"",b=null,I=null;async function U(e,l){try{let a=await B("/api/threads/token",{api_key:w,email:e,userHash:l});if(a.ok){let t=await a.json();if(t.widgetToken){d=t.widgetToken,localStorage.setItem("heedb_token",t.widgetToken);return}}d="",localStorage.removeItem("heedb_token")}catch(a){d="",localStorage.removeItem("heedb_token")}}window.Heedb={init(e={}){if(e.name&&(v=e.name,localStorage.setItem("heedb_name",e.name)),e.email){let l=e.email!==r;r=e.email,localStorage.setItem("heedb_email",e.email),e.userHash?U(e.email,e.userHash).then(()=>{z(),k&&b&&y()}):(z(),k&&b&&y())}}};function B(e,l){return fetch(`${H}${e}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(l)})}function D(e){let l=new URL(`${H}/api/threads`);return l.searchParams.set("api_key",w),l.searchParams.set("email",e),d&&l.searchParams.set("token",d),fetch(l.toString()).then(a=>a.json())}let K=`
2
+ "use strict";(()=>{(function(){var $,D,q,O,R;let y=document.currentScript||document.querySelector("script[data-api-key]"),k=($=y==null?void 0:y.getAttribute("data-api-key"))!=null?$:"",_=(D=y==null?void 0:y.src)!=null?D:"",C=(y==null?void 0:y.getAttribute("data-host"))||(_?new URL(_).origin:"");if(!k){console.warn("[Heedb] Missing data-api-key on <script> tag.");return}let M=!1,b="contact",I="",T=(q=localStorage.getItem("heedb_name"))!=null?q:"",m=(O=localStorage.getItem("heedb_email"))!=null?O:"",f=(R=localStorage.getItem("heedb_token"))!=null?R:"",v=null,P=null;async function F(e,t){try{let o=await S("/api/threads/token",{api_key:k,email:e,userHash:t});if(o.ok){let l=await o.json();if(l.widgetToken){f=l.widgetToken,localStorage.setItem("heedb_token",l.widgetToken);return}}f="",localStorage.removeItem("heedb_token")}catch(o){f="",localStorage.removeItem("heedb_token")}}window.Heedb={init(e={}){e.name&&(T=e.name,localStorage.setItem("heedb_name",e.name)),e.email&&(m=e.email,localStorage.setItem("heedb_email",e.email),e.userHash?F(e.email,e.userHash).then(()=>{B(),M&&v&&h()}):(B(),M&&v&&h()))}};function S(e,t){return fetch(`${C}${e}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})}function J(e){let t=new URL(`${C}/api/threads`);return t.searchParams.set("api_key",k),t.searchParams.set("email",e),f&&t.searchParams.set("token",f),fetch(t.toString()).then(o=>o.json())}function K(e){let t=new URL(`${C}/api/threads/${e}`);return t.searchParams.set("api_key",k),t.searchParams.set("email",m),t.searchParams.set("token",f),fetch(t.toString()).then(o=>o.json())}function U(e,t){return S(`/api/threads/${e}/reply`,{api_key:k,email:m,token:f,message:t})}let W=`
3
3
  .llp-btn {
4
4
  position: fixed; bottom: 24px; right: 24px; z-index: 999998;
5
5
  width: 52px; height: 52px; border-radius: 50%; border: none;
@@ -11,7 +11,7 @@
11
11
  .llp-btn:hover { transform: scale(1.08); }
12
12
  .llp-panel {
13
13
  position: fixed; bottom: 88px; right: 24px; z-index: 999999;
14
- width: 340px; max-height: 540px; border-radius: 16px;
14
+ width: 370px; max-height: 560px; border-radius: 16px;
15
15
  background: #fff; box-shadow: 0 8px 32px rgba(0,0,0,.18);
16
16
  display: flex; flex-direction: column; overflow: hidden;
17
17
  font-family: system-ui, -apple-system, sans-serif; font-size: 14px;
@@ -61,8 +61,8 @@
61
61
  .llp-success p { margin: 0; color: #71717a; font-size: 13px; line-height: 1.5; }
62
62
  .llp-thread-item {
63
63
  padding: 10px 12px; border: 1px solid #e4e4e7; border-radius: 8px;
64
- margin-bottom: 8px; text-decoration: none; display: block; color: inherit;
65
- transition: background .1s;
64
+ margin-bottom: 8px; cursor: pointer; display: block; color: inherit;
65
+ transition: background .1s; background: none;
66
66
  }
67
67
  .llp-thread-item:hover { background: #f4f4f5; }
68
68
  .llp-thread-label { font-size: 12px; color: #71717a; margin-bottom: 2px; }
@@ -77,8 +77,53 @@
77
77
  border-radius: 8px; font-size: 13px; cursor: pointer; color: #52525b;
78
78
  }
79
79
  .llp-new-msg button:hover { background: #f4f4f5; }
80
- `;function n(e,l={},a=[]){let t=document.createElement(e);for(let[o,s]of Object.entries(l))o==="class"?t.className=s:o==="style"?t.setAttribute("style",s):o.startsWith("on")&&typeof s=="function"?t.addEventListener(o.slice(2),s):t.setAttribute(o,String(s));for(let o of a)t.appendChild(typeof o=="string"?document.createTextNode(o):o);return t}function S(e,l,a=""){let t=document.createElement("input");return t.type=e,t.id=l,t.placeholder=a,t.className="llp-input",t}function F(e,l=""){let a=document.createElement("textarea");return a.id=e,a.placeholder=l,a.className="llp-textarea",a}function J(e,l){let a=document.createElement("select");a.id=e,a.className="llp-select";for(let t of l){let o=document.createElement("option");o.value=t.value,o.textContent=t.label,a.appendChild(o)}return a}function y(){if(!b)return;let e=b.querySelector(".llp-body");e.innerHTML="",x==="contact"?W(e):x==="privacy"?V(e):x==="threads"&&Y(e)}function W(e,l=""){let a=!!(v&&r),t=n("label",{class:"llp-label"},["Name"]),o=S("text","llp-name","Jane Smith");v&&(o.value=v);let s=n("label",{class:"llp-label"},["Email"]),m=S("email","llp-email","jane@example.com");(l||r)&&(m.value=l||r);let g=n("label",{class:"llp-label"},["Message"]),c=F("llp-message","How can we help?"),i=n("div",{class:"llp-error",style:"display:none"}),p=n("button",{class:"llp-btn-submit"},["Send message"]);p.onclick=async()=>{let f=o.value.trim(),h=m.value.trim(),E=c.value.trim();if(i.style.display="none",!f||!h||!E){i.textContent="Please fill in all fields.",i.style.display="block";return}p.disabled=!0,p.textContent="Sending\u2026";try{let C=await B("/api/contact",{api_key:w,name:f,email:h,message:E});if(C.ok)r=h,localStorage.setItem("heedb_email",h),z(),j(e,"Message sent!","We'll get back to you soon. Check your inbox for a confirmation email.");else{let G=await C.json();i.textContent=G.error||"Something went wrong.",i.style.display="block",p.disabled=!1,p.textContent="Send message"}}catch(C){i.textContent="Network error. Please try again.",i.style.display="block",p.disabled=!1,p.textContent="Send message"}},a?e.append(g,c,i,p):e.append(t,o,s,m,g,c,i,p)}function V(e){let l=!!(v&&r),a=n("label",{class:"llp-label"},["Name"]),t=S("text","llp-priv-name","Jane Smith");v&&(t.value=v);let o=n("label",{class:"llp-label"},["Email"]),s=S("email","llp-priv-email","jane@example.com");r&&(s.value=r);let m=n("label",{class:"llp-label"},["Request type"]),g=J("llp-priv-type",[{value:"deletion",label:"Delete my data"},{value:"access",label:"Access my data"},{value:"portability",label:"Export my data"},{value:"correction",label:"Correct my data"},{value:"restriction",label:"Restrict processing"},{value:"objection",label:"Object to processing"},{value:"other",label:"Other"}]),c=n("div",{class:"llp-error",style:"display:none"}),i=n("button",{class:"llp-btn-submit"},["Submit request"]);i.onclick=async()=>{let p=t.value.trim(),f=s.value.trim(),h=g.value;if(c.style.display="none",!p||!f){c.textContent="Please fill in all fields.",c.style.display="block";return}i.disabled=!0,i.textContent="Submitting\u2026";try{let E=await B("/api/privacy-request",{api_key:w,name:p,email:f,request_type:h});if(E.ok)r=f,localStorage.setItem("heedb_email",f),z(),j(e,"Request submitted!","Your privacy request has been received. We'll respond within 30 days.");else{let C=await E.json();c.textContent=C.error||"Something went wrong.",c.style.display="block",i.disabled=!1,i.textContent="Submit request"}}catch(E){c.textContent="Network error. Please try again.",c.style.display="block",i.disabled=!1,i.textContent="Submit request"}},l?e.append(m,g,c,i):e.append(a,t,o,s,m,g,c,i)}async function Y(e){var l;if(!d){e.innerHTML="";let a=n("p",{style:"color:#71717a;text-align:center;margin:8px 0;font-size:13px;line-height:1.5"},["Send a message below to view your message history."]),t=n("div",{class:"llp-new-msg"}),o=n("button",{},["+ Send a message"]);o.onclick=()=>{x="contact",L("contact"),y()},t.appendChild(o),e.append(a,t);return}e.innerHTML='<p style="color:#71717a;text-align:center">Loading\u2026</p>';try{let{threads:a}=await D(r);if(e.innerHTML="",a.length===0){let s=n("p",{style:"color:#71717a;text-align:center;margin:8px 0"},["No open messages yet."]);e.appendChild(s)}else for(let s of a){let m=n("span",{class:`llp-badge llp-badge-${s.status}`},[s.status]),g=n("div",{class:"llp-thread-label"},[s.type==="privacy"?"Privacy request":"Message"," \u2022 "]);g.appendChild(m);let c=n("div",{class:"llp-thread-text"},[(l=s.preview)!=null?l:""]),i=new Date(s.createdAt).toLocaleDateString(),p=n("div",{class:"llp-thread-meta"},[i]),f=`${H}/t/${encodeURIComponent(s.id)}?email=${encodeURIComponent(r)}&token=${encodeURIComponent(d)}&apiKey=${encodeURIComponent(w)}`,h=n("a",{class:"llp-thread-item",href:f,target:"_blank",rel:"noopener noreferrer"});h.append(g,c,p),e.appendChild(h)}let t=n("div",{class:"llp-new-msg"}),o=n("button",{},["+ Send a new message"]);o.onclick=()=>{x="contact",L("contact"),y()},t.appendChild(o),e.appendChild(t)}catch(a){e.innerHTML='<p style="color:#ef4444;text-align:center">Failed to load messages.</p>'}}function j(e,l,a){e.innerHTML="";let t=n("div",{class:"llp-success"});t.innerHTML=`
80
+
81
+ /* Chat view */
82
+ .llp-chat-back {
83
+ background: none; border: none; cursor: pointer; font-size: 13px;
84
+ color: #6366f1; padding: 0; margin-bottom: 12px; font-weight: 500;
85
+ display: flex; align-items: center; gap: 4px;
86
+ }
87
+ .llp-chat-back:hover { text-decoration: underline; }
88
+ .llp-chat-messages {
89
+ display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px;
90
+ max-height: 320px; overflow-y: auto;
91
+ }
92
+ .llp-chat-bubble {
93
+ padding: 8px 12px; border-radius: 12px; font-size: 13px;
94
+ line-height: 1.45; max-width: 85%; word-wrap: break-word;
95
+ white-space: pre-wrap;
96
+ }
97
+ .llp-chat-inbound {
98
+ background: #f4f4f5; color: #18181b; align-self: flex-start;
99
+ border-bottom-left-radius: 4px;
100
+ }
101
+ .llp-chat-outbound {
102
+ background: #18181b; color: #fff; align-self: flex-end;
103
+ border-bottom-right-radius: 4px;
104
+ }
105
+ .llp-chat-time {
106
+ font-size: 10px; color: #a1a1aa; margin-top: 2px;
107
+ }
108
+ .llp-chat-time-right { text-align: right; }
109
+ .llp-chat-reply {
110
+ display: flex; gap: 8px; border-top: 1px solid #e4e4e7; padding-top: 12px;
111
+ }
112
+ .llp-chat-reply-input {
113
+ flex: 1; padding: 8px 10px; border: 1px solid #d4d4d8; border-radius: 8px;
114
+ font-size: 13px; font-family: inherit; color: #18181b;
115
+ resize: none; min-height: 36px; max-height: 80px; box-sizing: border-box;
116
+ }
117
+ .llp-chat-reply-input:focus { outline: none; border-color: #18181b; }
118
+ .llp-chat-send {
119
+ padding: 8px 14px; background: #18181b; color: #fff; border: none;
120
+ border-radius: 8px; font-size: 13px; font-weight: 600; cursor: pointer;
121
+ transition: opacity .15s; white-space: nowrap; align-self: flex-end;
122
+ }
123
+ .llp-chat-send:hover { opacity: .85; }
124
+ .llp-chat-send:disabled { opacity: .5; cursor: not-allowed; }
125
+ `;function n(e,t={},o=[]){let l=document.createElement(e);for(let[a,s]of Object.entries(t))a==="class"?l.className=s:a==="style"?l.setAttribute("style",s):a.startsWith("on")&&typeof s=="function"?l.addEventListener(a.slice(2),s):l.setAttribute(a,String(s));for(let a of o)l.appendChild(typeof a=="string"?document.createTextNode(a):a);return l}function z(e,t,o=""){let l=document.createElement("input");return l.type=e,l.id=t,l.placeholder=o,l.className="llp-input",l}function V(e,t=""){let o=document.createElement("textarea");return o.id=e,o.placeholder=t,o.className="llp-textarea",o}function Y(e,t){let o=document.createElement("select");o.id=e,o.className="llp-select";for(let l of t){let a=document.createElement("option");a.value=l.value,a.textContent=l.label,o.appendChild(a)}return o}function h(){if(!v)return;let e=v.querySelector(".llp-body");e.innerHTML="",b==="contact"?G(e):b==="privacy"?Q(e):b==="threads"?X(e):b==="thread-detail"&&ee(e),L&&(L.style.display=b==="thread-detail"?"none":"flex")}function G(e,t=""){let o=!!(T&&m),l=n("label",{class:"llp-label"},["Name"]),a=z("text","llp-name","Jane Smith");T&&(a.value=T);let s=n("label",{class:"llp-label"},["Email"]),r=z("email","llp-email","jane@example.com");(t||m)&&(r.value=t||m);let u=n("label",{class:"llp-label"},["Message"]),p=V("llp-message","How can we help?"),c=n("div",{class:"llp-error",style:"display:none"}),i=n("button",{class:"llp-btn-submit"},["Send message"]);i.onclick=async()=>{let d=a.value.trim(),g=r.value.trim(),x=p.value.trim();if(c.style.display="none",!d||!g||!x){c.textContent="Please fill in all fields.",c.style.display="block";return}i.disabled=!0,i.textContent="Sending\u2026";try{let w=await S("/api/contact",{api_key:k,name:d,email:g,message:x});if(w.ok)m=g,localStorage.setItem("heedb_email",g),B(),A(e,"Message sent!","We'll get back to you soon. Check your inbox for a confirmation email.");else{let ne=await w.json();c.textContent=ne.error||"Something went wrong.",c.style.display="block",i.disabled=!1,i.textContent="Send message"}}catch(w){c.textContent="Network error. Please try again.",c.style.display="block",i.disabled=!1,i.textContent="Send message"}},o?e.append(u,p,c,i):e.append(l,a,s,r,u,p,c,i)}function Q(e){let t=!!(T&&m),o=n("label",{class:"llp-label"},["Name"]),l=z("text","llp-priv-name","Jane Smith");T&&(l.value=T);let a=n("label",{class:"llp-label"},["Email"]),s=z("email","llp-priv-email","jane@example.com");m&&(s.value=m);let r=n("label",{class:"llp-label"},["Request type"]),u=Y("llp-priv-type",[{value:"deletion",label:"Delete my data"},{value:"access",label:"Access my data"},{value:"portability",label:"Export my data"},{value:"correction",label:"Correct my data"},{value:"restriction",label:"Restrict processing"},{value:"objection",label:"Object to processing"},{value:"other",label:"Other"}]),p=n("div",{class:"llp-error",style:"display:none"}),c=n("button",{class:"llp-btn-submit"},["Submit request"]);c.onclick=async()=>{let i=l.value.trim(),d=s.value.trim(),g=u.value;if(p.style.display="none",!i||!d){p.textContent="Please fill in all fields.",p.style.display="block";return}c.disabled=!0,c.textContent="Submitting\u2026";try{let x=await S("/api/privacy-request",{api_key:k,name:i,email:d,request_type:g});if(x.ok)m=d,localStorage.setItem("heedb_email",d),B(),A(e,"Request submitted!","Your privacy request has been received. We'll respond within 30 days.");else{let w=await x.json();p.textContent=w.error||"Something went wrong.",p.style.display="block",c.disabled=!1,c.textContent="Submit request"}}catch(x){p.textContent="Network error. Please try again.",p.style.display="block",c.disabled=!1,c.textContent="Submit request"}},t?e.append(r,u,p,c):e.append(o,l,a,s,r,u,p,c)}async function X(e){var t;if(!f){e.innerHTML="";let o=n("p",{style:"color:#71717a;text-align:center;margin:8px 0;font-size:13px;line-height:1.5"},["Send a message below to view your message history."]),l=n("div",{class:"llp-new-msg"}),a=n("button",{},["+ Send a message"]);a.onclick=()=>{b="contact",E("contact"),h()},l.appendChild(a),e.append(o,l);return}e.innerHTML='<p style="color:#71717a;text-align:center">Loading\u2026</p>';try{let{threads:o}=await J(m);if(e.innerHTML="",o.length===0){let s=n("p",{style:"color:#71717a;text-align:center;margin:8px 0"},["No open messages yet."]);e.appendChild(s)}else for(let s of o){let r=n("span",{class:`llp-badge llp-badge-${s.status}`},[s.status]),u=n("div",{class:"llp-thread-label"},[s.type==="privacy"?"Privacy request":"Message"," \u2022 "]);u.appendChild(r);let p=n("div",{class:"llp-thread-text"},[(t=s.preview)!=null?t:""]),c=new Date(s.createdAt).toLocaleDateString(),i=n("div",{class:"llp-thread-meta"},[c]),d=n("div",{class:"llp-thread-item"});d.append(u,p,i),d.onclick=()=>Z(s.id),e.appendChild(d)}let l=n("div",{class:"llp-new-msg"}),a=n("button",{},["+ Send a new message"]);a.onclick=()=>{b="contact",E("contact"),h()},l.appendChild(a),e.appendChild(l)}catch(o){e.innerHTML='<p style="color:#ef4444;text-align:center">Failed to load messages.</p>'}}function Z(e){I=e,b="thread-detail",h()}async function ee(e){e.innerHTML='<p style="color:#71717a;text-align:center">Loading\u2026</p>';let t=n("button",{class:"llp-chat-back"},["\u2190 Back to messages"]);t.onclick=()=>{b="threads",E("threads"),h()};try{let l=(await K(I)).messages||[];e.innerHTML="",e.style.padding="12px",e.appendChild(t);let a=n("div",{class:"llp-chat-messages"});for(let i of l){let d=i.direction==="inbound",g=n("div",{style:`display:flex;flex-direction:column;${d?"align-items:flex-start":"align-items:flex-end"}`}),x=n("div",{class:`llp-chat-bubble ${d?"llp-chat-inbound":"llp-chat-outbound"}`},[i.body]),w=n("div",{class:`llp-chat-time ${d?"":"llp-chat-time-right"}`},[te(i.createdAt)]);g.append(x,w),a.appendChild(g)}e.appendChild(a),requestAnimationFrame(()=>{a.scrollTop=a.scrollHeight});let s=n("div",{class:"llp-chat-reply"}),r=document.createElement("textarea");r.className="llp-chat-reply-input",r.placeholder="Type a reply\u2026",r.rows=1,r.addEventListener("input",()=>{r.style.height="auto",r.style.height=Math.min(r.scrollHeight,80)+"px"});let u=n("button",{class:"llp-chat-send"},["Send"]),p=n("div",{class:"llp-error",style:"display:none;margin-top:4px"});async function c(){let i=r.value.trim();if(i){u.disabled=!0,u.textContent="\u2026",p.style.display="none";try{let d=await U(I,i);if(d.ok){let g=n("div",{style:"display:flex;flex-direction:column;align-items:flex-start"}),x=n("div",{class:"llp-chat-bubble llp-chat-inbound"},[i]),w=n("div",{class:"llp-chat-time"},["Just now"]);g.append(x,w),a.appendChild(g),a.scrollTop=a.scrollHeight,r.value="",r.style.height="auto"}else{let g=await d.json();p.textContent=g.error||"Failed to send.",p.style.display="block"}}catch(d){p.textContent="Network error. Please try again.",p.style.display="block"}u.disabled=!1,u.textContent="Send"}}u.onclick=c,r.addEventListener("keydown",i=>{i.key==="Enter"&&!i.shiftKey&&(i.preventDefault(),c())}),s.append(r,u),e.append(s,p),requestAnimationFrame(()=>r.focus())}catch(o){e.innerHTML="",e.appendChild(t),e.appendChild(n("p",{style:"color:#ef4444;text-align:center"},["Failed to load conversation."]))}}function te(e){let t=new Date(e),l=new Date().getTime()-t.getTime(),a=Math.floor(l/6e4);if(a<1)return"Just now";if(a<60)return`${a}m ago`;let s=Math.floor(a/60);return s<24?`${s}h ago`:t.toLocaleDateString()}function A(e,t,o){e.innerHTML="";let l=n("div",{class:"llp-success"});l.innerHTML=`
81
126
  <div class="llp-success-icon">\u2705</div>
82
- <h3>${l}</h3>
83
- <p>${a}</p>
84
- `;let o=n("a",{href:`${H}/app?email=${encodeURIComponent(r)}`,target:"_blank",style:"display:inline-block;margin-top:12px;color:#18181b;font-size:13px;font-weight:600"},["View your messages \u2192"]);t.appendChild(o),e.appendChild(t)}let M=null,T=null;function L(e){x=e,M&&M.querySelectorAll(".llp-tab").forEach(l=>{l.classList.toggle("active",l.dataset.mode===e)})}function z(){T&&(T.style.display=r&&d?"":"none")}function N(){let e=document.createElement("style");e.textContent=K,document.head.appendChild(e),I=n("button",{class:"llp-btn",title:"Contact us","aria-label":"Open contact widget"},["\u{1F4AC}"]),I.onclick=P,document.body.appendChild(I),b=n("div",{class:"llp-panel",style:"display:none"});let l=n("div",{class:"llp-header"}),a=n("h2",{},["Contact us"]),t=n("button",{class:"llp-close","aria-label":"Close"},["\u2715"]);t.onclick=P,l.append(a,t),M=n("div",{class:"llp-tabs"});let o=n("button",{class:"llp-tab active","data-mode":"contact"},["Message"]),s=n("button",{class:"llp-tab","data-mode":"privacy"},["Privacy"]);T=n("button",{class:"llp-tab","data-mode":"threads"},["Messages"]),T.style.display=r&&d?"":"none",o.onclick=()=>{L("contact"),y()},s.onclick=()=>{L("privacy"),y()},T.onclick=()=>{L("threads"),y()},M.append(o,s,T);let m=n("div",{class:"llp-body"});b.append(l,M,m),document.body.appendChild(b)}function P(){k=!k,b&&(b.style.display=k?"flex":"none",k&&(r&&d&&x!=="privacy"&&(x="threads",L("threads")),y()))}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",N):N()})();})();
127
+ <h3>${t}</h3>
128
+ <p>${o}</p>
129
+ `;let a=n("a",{href:`${C}/app?email=${encodeURIComponent(m)}`,target:"_blank",style:"display:inline-block;margin-top:12px;color:#18181b;font-size:13px;font-weight:600"},["View your messages \u2192"]);l.appendChild(a),e.appendChild(l)}let L=null,H=null;function E(e){b=e,L&&L.querySelectorAll(".llp-tab").forEach(t=>{t.classList.toggle("active",t.dataset.mode===e)})}function B(){H&&(H.style.display=m&&f?"":"none")}function j(){let e=document.createElement("style");e.textContent=W,document.head.appendChild(e),P=n("button",{class:"llp-btn",title:"Contact us","aria-label":"Open contact widget"},["\u{1F4AC}"]),P.onclick=N,document.body.appendChild(P),v=n("div",{class:"llp-panel",style:"display:none"});let t=n("div",{class:"llp-header"}),o=n("h2",{},["Contact us"]),l=n("button",{class:"llp-close","aria-label":"Close"},["\u2715"]);l.onclick=N,t.append(o,l),L=n("div",{class:"llp-tabs"});let a=n("button",{class:"llp-tab active","data-mode":"contact"},["Message"]),s=n("button",{class:"llp-tab","data-mode":"privacy"},["Privacy"]);H=n("button",{class:"llp-tab","data-mode":"threads"},["Messages"]),H.style.display=m&&f?"":"none",a.onclick=()=>{E("contact"),h()},s.onclick=()=>{E("privacy"),h()},H.onclick=()=>{E("threads"),h()},L.append(a,s,H);let r=n("div",{class:"llp-body"});v.append(t,L,r),document.body.appendChild(v)}function N(){M=!M,v&&(v.style.display=M?"flex":"none",M&&(m&&f&&b!=="privacy"&&b!=="thread-detail"&&(b="threads",E("threads")),h()))}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",j):j()})();})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heedb/web-sdk",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Drop-in feedback widget with email conversations for any website",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -24,6 +24,7 @@
24
24
  },
25
25
  "files": [
26
26
  "dist",
27
+ "widget.js",
27
28
  "README.md"
28
29
  ],
29
30
  "scripts": {
package/widget.js ADDED
@@ -0,0 +1,129 @@
1
+ /* Heedb Widget — https://heedb.com */
2
+ "use strict";(()=>{(function(){var $,D,q,O,R;let y=document.currentScript||document.querySelector("script[data-api-key]"),k=($=y==null?void 0:y.getAttribute("data-api-key"))!=null?$:"",_=(D=y==null?void 0:y.src)!=null?D:"",C=(y==null?void 0:y.getAttribute("data-host"))||(_?new URL(_).origin:"");if(!k){console.warn("[Heedb] Missing data-api-key on <script> tag.");return}let M=!1,b="contact",I="",T=(q=localStorage.getItem("heedb_name"))!=null?q:"",m=(O=localStorage.getItem("heedb_email"))!=null?O:"",f=(R=localStorage.getItem("heedb_token"))!=null?R:"",v=null,P=null;async function F(e,t){try{let o=await S("/api/threads/token",{api_key:k,email:e,userHash:t});if(o.ok){let l=await o.json();if(l.widgetToken){f=l.widgetToken,localStorage.setItem("heedb_token",l.widgetToken);return}}f="",localStorage.removeItem("heedb_token")}catch(o){f="",localStorage.removeItem("heedb_token")}}window.Heedb={init(e={}){e.name&&(T=e.name,localStorage.setItem("heedb_name",e.name)),e.email&&(m=e.email,localStorage.setItem("heedb_email",e.email),e.userHash?F(e.email,e.userHash).then(()=>{B(),M&&v&&h()}):(B(),M&&v&&h()))}};function S(e,t){return fetch(`${C}${e}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})}function J(e){let t=new URL(`${C}/api/threads`);return t.searchParams.set("api_key",k),t.searchParams.set("email",e),f&&t.searchParams.set("token",f),fetch(t.toString()).then(o=>o.json())}function K(e){let t=new URL(`${C}/api/threads/${e}`);return t.searchParams.set("api_key",k),t.searchParams.set("email",m),t.searchParams.set("token",f),fetch(t.toString()).then(o=>o.json())}function U(e,t){return S(`/api/threads/${e}/reply`,{api_key:k,email:m,token:f,message:t})}let W=`
3
+ .llp-btn {
4
+ position: fixed; bottom: 24px; right: 24px; z-index: 999998;
5
+ width: 52px; height: 52px; border-radius: 50%; border: none;
6
+ background: #18181b; color: #fff; font-size: 22px; cursor: pointer;
7
+ box-shadow: 0 4px 16px rgba(0,0,0,.25);
8
+ display: flex; align-items: center; justify-content: center;
9
+ transition: transform .15s;
10
+ }
11
+ .llp-btn:hover { transform: scale(1.08); }
12
+ .llp-panel {
13
+ position: fixed; bottom: 88px; right: 24px; z-index: 999999;
14
+ width: 370px; max-height: 560px; border-radius: 16px;
15
+ background: #fff; box-shadow: 0 8px 32px rgba(0,0,0,.18);
16
+ display: flex; flex-direction: column; overflow: hidden;
17
+ font-family: system-ui, -apple-system, sans-serif; font-size: 14px;
18
+ transition: opacity .15s, transform .15s;
19
+ }
20
+ .llp-header {
21
+ background: #18181b; color: #fff; padding: 14px 16px;
22
+ display: flex; align-items: center; justify-content: space-between;
23
+ }
24
+ .llp-header h2 { margin: 0; font-size: 15px; font-weight: 600; }
25
+ .llp-close {
26
+ background: none; border: none; color: #fff; cursor: pointer;
27
+ font-size: 18px; line-height: 1; padding: 0;
28
+ }
29
+ .llp-tabs {
30
+ display: flex; border-bottom: 1px solid #e4e4e7;
31
+ }
32
+ .llp-tab {
33
+ flex: 1; padding: 10px 0; border: none; background: none;
34
+ cursor: pointer; font-size: 13px; font-weight: 500; color: #71717a;
35
+ border-bottom: 2px solid transparent; margin-bottom: -1px;
36
+ }
37
+ .llp-tab.active { color: #18181b; border-bottom-color: #18181b; }
38
+ .llp-body { padding: 16px; overflow-y: auto; flex: 1; }
39
+ .llp-label { display: block; font-size: 12px; font-weight: 600; color: #52525b; margin-bottom: 4px; }
40
+ .llp-input, .llp-textarea, .llp-select {
41
+ width: 100%; box-sizing: border-box; padding: 8px 10px;
42
+ border: 1px solid #d4d4d8; border-radius: 8px;
43
+ font-size: 13px; color: #18181b; margin-bottom: 10px;
44
+ font-family: inherit; background: #fff;
45
+ }
46
+ .llp-textarea { min-height: 80px; resize: vertical; }
47
+ .llp-input:focus, .llp-textarea:focus, .llp-select:focus {
48
+ outline: none; border-color: #18181b;
49
+ }
50
+ .llp-btn-submit {
51
+ width: 100%; padding: 10px; background: #18181b; color: #fff;
52
+ border: none; border-radius: 8px; font-size: 14px; font-weight: 600;
53
+ cursor: pointer; transition: opacity .15s;
54
+ }
55
+ .llp-btn-submit:hover { opacity: .85; }
56
+ .llp-btn-submit:disabled { opacity: .5; cursor: not-allowed; }
57
+ .llp-error { color: #ef4444; font-size: 12px; margin-bottom: 8px; }
58
+ .llp-success { text-align: center; padding: 24px 8px; }
59
+ .llp-success-icon { font-size: 36px; }
60
+ .llp-success h3 { margin: 12px 0 6px; font-size: 15px; color: #18181b; }
61
+ .llp-success p { margin: 0; color: #71717a; font-size: 13px; line-height: 1.5; }
62
+ .llp-thread-item {
63
+ padding: 10px 12px; border: 1px solid #e4e4e7; border-radius: 8px;
64
+ margin-bottom: 8px; cursor: pointer; display: block; color: inherit;
65
+ transition: background .1s; background: none;
66
+ }
67
+ .llp-thread-item:hover { background: #f4f4f5; }
68
+ .llp-thread-label { font-size: 12px; color: #71717a; margin-bottom: 2px; }
69
+ .llp-thread-text { font-size: 13px; color: #18181b; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
70
+ .llp-thread-meta { font-size: 11px; color: #a1a1aa; margin-top: 3px; }
71
+ .llp-badge { display: inline-block; padding: 1px 6px; border-radius: 9999px; font-size: 10px; font-weight: 600; }
72
+ .llp-badge-open { background: #d1fae5; color: #065f46; }
73
+ .llp-badge-replied { background: #dbeafe; color: #1e40af; }
74
+ .llp-new-msg { margin-top: 12px; padding-top: 12px; border-top: 1px solid #e4e4e7; }
75
+ .llp-new-msg button {
76
+ width: 100%; padding: 8px; background: none; border: 1px solid #d4d4d8;
77
+ border-radius: 8px; font-size: 13px; cursor: pointer; color: #52525b;
78
+ }
79
+ .llp-new-msg button:hover { background: #f4f4f5; }
80
+
81
+ /* Chat view */
82
+ .llp-chat-back {
83
+ background: none; border: none; cursor: pointer; font-size: 13px;
84
+ color: #6366f1; padding: 0; margin-bottom: 12px; font-weight: 500;
85
+ display: flex; align-items: center; gap: 4px;
86
+ }
87
+ .llp-chat-back:hover { text-decoration: underline; }
88
+ .llp-chat-messages {
89
+ display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px;
90
+ max-height: 320px; overflow-y: auto;
91
+ }
92
+ .llp-chat-bubble {
93
+ padding: 8px 12px; border-radius: 12px; font-size: 13px;
94
+ line-height: 1.45; max-width: 85%; word-wrap: break-word;
95
+ white-space: pre-wrap;
96
+ }
97
+ .llp-chat-inbound {
98
+ background: #f4f4f5; color: #18181b; align-self: flex-start;
99
+ border-bottom-left-radius: 4px;
100
+ }
101
+ .llp-chat-outbound {
102
+ background: #18181b; color: #fff; align-self: flex-end;
103
+ border-bottom-right-radius: 4px;
104
+ }
105
+ .llp-chat-time {
106
+ font-size: 10px; color: #a1a1aa; margin-top: 2px;
107
+ }
108
+ .llp-chat-time-right { text-align: right; }
109
+ .llp-chat-reply {
110
+ display: flex; gap: 8px; border-top: 1px solid #e4e4e7; padding-top: 12px;
111
+ }
112
+ .llp-chat-reply-input {
113
+ flex: 1; padding: 8px 10px; border: 1px solid #d4d4d8; border-radius: 8px;
114
+ font-size: 13px; font-family: inherit; color: #18181b;
115
+ resize: none; min-height: 36px; max-height: 80px; box-sizing: border-box;
116
+ }
117
+ .llp-chat-reply-input:focus { outline: none; border-color: #18181b; }
118
+ .llp-chat-send {
119
+ padding: 8px 14px; background: #18181b; color: #fff; border: none;
120
+ border-radius: 8px; font-size: 13px; font-weight: 600; cursor: pointer;
121
+ transition: opacity .15s; white-space: nowrap; align-self: flex-end;
122
+ }
123
+ .llp-chat-send:hover { opacity: .85; }
124
+ .llp-chat-send:disabled { opacity: .5; cursor: not-allowed; }
125
+ `;function n(e,t={},o=[]){let l=document.createElement(e);for(let[a,s]of Object.entries(t))a==="class"?l.className=s:a==="style"?l.setAttribute("style",s):a.startsWith("on")&&typeof s=="function"?l.addEventListener(a.slice(2),s):l.setAttribute(a,String(s));for(let a of o)l.appendChild(typeof a=="string"?document.createTextNode(a):a);return l}function z(e,t,o=""){let l=document.createElement("input");return l.type=e,l.id=t,l.placeholder=o,l.className="llp-input",l}function V(e,t=""){let o=document.createElement("textarea");return o.id=e,o.placeholder=t,o.className="llp-textarea",o}function Y(e,t){let o=document.createElement("select");o.id=e,o.className="llp-select";for(let l of t){let a=document.createElement("option");a.value=l.value,a.textContent=l.label,o.appendChild(a)}return o}function h(){if(!v)return;let e=v.querySelector(".llp-body");e.innerHTML="",b==="contact"?G(e):b==="privacy"?Q(e):b==="threads"?X(e):b==="thread-detail"&&ee(e),L&&(L.style.display=b==="thread-detail"?"none":"flex")}function G(e,t=""){let o=!!(T&&m),l=n("label",{class:"llp-label"},["Name"]),a=z("text","llp-name","Jane Smith");T&&(a.value=T);let s=n("label",{class:"llp-label"},["Email"]),r=z("email","llp-email","jane@example.com");(t||m)&&(r.value=t||m);let u=n("label",{class:"llp-label"},["Message"]),p=V("llp-message","How can we help?"),c=n("div",{class:"llp-error",style:"display:none"}),i=n("button",{class:"llp-btn-submit"},["Send message"]);i.onclick=async()=>{let d=a.value.trim(),g=r.value.trim(),x=p.value.trim();if(c.style.display="none",!d||!g||!x){c.textContent="Please fill in all fields.",c.style.display="block";return}i.disabled=!0,i.textContent="Sending\u2026";try{let w=await S("/api/contact",{api_key:k,name:d,email:g,message:x});if(w.ok)m=g,localStorage.setItem("heedb_email",g),B(),A(e,"Message sent!","We'll get back to you soon. Check your inbox for a confirmation email.");else{let ne=await w.json();c.textContent=ne.error||"Something went wrong.",c.style.display="block",i.disabled=!1,i.textContent="Send message"}}catch(w){c.textContent="Network error. Please try again.",c.style.display="block",i.disabled=!1,i.textContent="Send message"}},o?e.append(u,p,c,i):e.append(l,a,s,r,u,p,c,i)}function Q(e){let t=!!(T&&m),o=n("label",{class:"llp-label"},["Name"]),l=z("text","llp-priv-name","Jane Smith");T&&(l.value=T);let a=n("label",{class:"llp-label"},["Email"]),s=z("email","llp-priv-email","jane@example.com");m&&(s.value=m);let r=n("label",{class:"llp-label"},["Request type"]),u=Y("llp-priv-type",[{value:"deletion",label:"Delete my data"},{value:"access",label:"Access my data"},{value:"portability",label:"Export my data"},{value:"correction",label:"Correct my data"},{value:"restriction",label:"Restrict processing"},{value:"objection",label:"Object to processing"},{value:"other",label:"Other"}]),p=n("div",{class:"llp-error",style:"display:none"}),c=n("button",{class:"llp-btn-submit"},["Submit request"]);c.onclick=async()=>{let i=l.value.trim(),d=s.value.trim(),g=u.value;if(p.style.display="none",!i||!d){p.textContent="Please fill in all fields.",p.style.display="block";return}c.disabled=!0,c.textContent="Submitting\u2026";try{let x=await S("/api/privacy-request",{api_key:k,name:i,email:d,request_type:g});if(x.ok)m=d,localStorage.setItem("heedb_email",d),B(),A(e,"Request submitted!","Your privacy request has been received. We'll respond within 30 days.");else{let w=await x.json();p.textContent=w.error||"Something went wrong.",p.style.display="block",c.disabled=!1,c.textContent="Submit request"}}catch(x){p.textContent="Network error. Please try again.",p.style.display="block",c.disabled=!1,c.textContent="Submit request"}},t?e.append(r,u,p,c):e.append(o,l,a,s,r,u,p,c)}async function X(e){var t;if(!f){e.innerHTML="";let o=n("p",{style:"color:#71717a;text-align:center;margin:8px 0;font-size:13px;line-height:1.5"},["Send a message below to view your message history."]),l=n("div",{class:"llp-new-msg"}),a=n("button",{},["+ Send a message"]);a.onclick=()=>{b="contact",E("contact"),h()},l.appendChild(a),e.append(o,l);return}e.innerHTML='<p style="color:#71717a;text-align:center">Loading\u2026</p>';try{let{threads:o}=await J(m);if(e.innerHTML="",o.length===0){let s=n("p",{style:"color:#71717a;text-align:center;margin:8px 0"},["No open messages yet."]);e.appendChild(s)}else for(let s of o){let r=n("span",{class:`llp-badge llp-badge-${s.status}`},[s.status]),u=n("div",{class:"llp-thread-label"},[s.type==="privacy"?"Privacy request":"Message"," \u2022 "]);u.appendChild(r);let p=n("div",{class:"llp-thread-text"},[(t=s.preview)!=null?t:""]),c=new Date(s.createdAt).toLocaleDateString(),i=n("div",{class:"llp-thread-meta"},[c]),d=n("div",{class:"llp-thread-item"});d.append(u,p,i),d.onclick=()=>Z(s.id),e.appendChild(d)}let l=n("div",{class:"llp-new-msg"}),a=n("button",{},["+ Send a new message"]);a.onclick=()=>{b="contact",E("contact"),h()},l.appendChild(a),e.appendChild(l)}catch(o){e.innerHTML='<p style="color:#ef4444;text-align:center">Failed to load messages.</p>'}}function Z(e){I=e,b="thread-detail",h()}async function ee(e){e.innerHTML='<p style="color:#71717a;text-align:center">Loading\u2026</p>';let t=n("button",{class:"llp-chat-back"},["\u2190 Back to messages"]);t.onclick=()=>{b="threads",E("threads"),h()};try{let l=(await K(I)).messages||[];e.innerHTML="",e.style.padding="12px",e.appendChild(t);let a=n("div",{class:"llp-chat-messages"});for(let i of l){let d=i.direction==="inbound",g=n("div",{style:`display:flex;flex-direction:column;${d?"align-items:flex-start":"align-items:flex-end"}`}),x=n("div",{class:`llp-chat-bubble ${d?"llp-chat-inbound":"llp-chat-outbound"}`},[i.body]),w=n("div",{class:`llp-chat-time ${d?"":"llp-chat-time-right"}`},[te(i.createdAt)]);g.append(x,w),a.appendChild(g)}e.appendChild(a),requestAnimationFrame(()=>{a.scrollTop=a.scrollHeight});let s=n("div",{class:"llp-chat-reply"}),r=document.createElement("textarea");r.className="llp-chat-reply-input",r.placeholder="Type a reply\u2026",r.rows=1,r.addEventListener("input",()=>{r.style.height="auto",r.style.height=Math.min(r.scrollHeight,80)+"px"});let u=n("button",{class:"llp-chat-send"},["Send"]),p=n("div",{class:"llp-error",style:"display:none;margin-top:4px"});async function c(){let i=r.value.trim();if(i){u.disabled=!0,u.textContent="\u2026",p.style.display="none";try{let d=await U(I,i);if(d.ok){let g=n("div",{style:"display:flex;flex-direction:column;align-items:flex-start"}),x=n("div",{class:"llp-chat-bubble llp-chat-inbound"},[i]),w=n("div",{class:"llp-chat-time"},["Just now"]);g.append(x,w),a.appendChild(g),a.scrollTop=a.scrollHeight,r.value="",r.style.height="auto"}else{let g=await d.json();p.textContent=g.error||"Failed to send.",p.style.display="block"}}catch(d){p.textContent="Network error. Please try again.",p.style.display="block"}u.disabled=!1,u.textContent="Send"}}u.onclick=c,r.addEventListener("keydown",i=>{i.key==="Enter"&&!i.shiftKey&&(i.preventDefault(),c())}),s.append(r,u),e.append(s,p),requestAnimationFrame(()=>r.focus())}catch(o){e.innerHTML="",e.appendChild(t),e.appendChild(n("p",{style:"color:#ef4444;text-align:center"},["Failed to load conversation."]))}}function te(e){let t=new Date(e),l=new Date().getTime()-t.getTime(),a=Math.floor(l/6e4);if(a<1)return"Just now";if(a<60)return`${a}m ago`;let s=Math.floor(a/60);return s<24?`${s}h ago`:t.toLocaleDateString()}function A(e,t,o){e.innerHTML="";let l=n("div",{class:"llp-success"});l.innerHTML=`
126
+ <div class="llp-success-icon">\u2705</div>
127
+ <h3>${t}</h3>
128
+ <p>${o}</p>
129
+ `;let a=n("a",{href:`${C}/app?email=${encodeURIComponent(m)}`,target:"_blank",style:"display:inline-block;margin-top:12px;color:#18181b;font-size:13px;font-weight:600"},["View your messages \u2192"]);l.appendChild(a),e.appendChild(l)}let L=null,H=null;function E(e){b=e,L&&L.querySelectorAll(".llp-tab").forEach(t=>{t.classList.toggle("active",t.dataset.mode===e)})}function B(){H&&(H.style.display=m&&f?"":"none")}function j(){let e=document.createElement("style");e.textContent=W,document.head.appendChild(e),P=n("button",{class:"llp-btn",title:"Contact us","aria-label":"Open contact widget"},["\u{1F4AC}"]),P.onclick=N,document.body.appendChild(P),v=n("div",{class:"llp-panel",style:"display:none"});let t=n("div",{class:"llp-header"}),o=n("h2",{},["Contact us"]),l=n("button",{class:"llp-close","aria-label":"Close"},["\u2715"]);l.onclick=N,t.append(o,l),L=n("div",{class:"llp-tabs"});let a=n("button",{class:"llp-tab active","data-mode":"contact"},["Message"]),s=n("button",{class:"llp-tab","data-mode":"privacy"},["Privacy"]);H=n("button",{class:"llp-tab","data-mode":"threads"},["Messages"]),H.style.display=m&&f?"":"none",a.onclick=()=>{E("contact"),h()},s.onclick=()=>{E("privacy"),h()},H.onclick=()=>{E("threads"),h()},L.append(a,s,H);let r=n("div",{class:"llp-body"});v.append(t,L,r),document.body.appendChild(v)}function N(){M=!M,v&&(v.style.display=M?"flex":"none",M&&(m&&f&&b!=="privacy"&&b!=="thread-detail"&&(b="threads",E("threads")),h()))}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",j):j()})();})();