@malto/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1804 @@
1
+ 'use strict';
2
+
3
+ // src/client.ts
4
+ var DEFAULT_API_URL = "https://api.malto.io";
5
+ var MaltoClient = class {
6
+ constructor(opts) {
7
+ this.apiKey = opts.apiKey;
8
+ this.apiUrl = (opts.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, "");
9
+ this.getSessionToken = opts.getSessionToken;
10
+ }
11
+ async request(path, init2 = {}) {
12
+ const headers = {
13
+ "Content-Type": "application/json",
14
+ Authorization: `Bearer ${this.apiKey}`
15
+ };
16
+ if (init2.headers) {
17
+ const extras = init2.headers;
18
+ for (const [k, v] of Object.entries(extras)) headers[k] = v;
19
+ }
20
+ if (init2.auth) {
21
+ const token = this.getSessionToken();
22
+ if (token) headers["X-Malto-Session"] = token;
23
+ }
24
+ const res = await fetch(`${this.apiUrl}${path}`, {
25
+ ...init2,
26
+ headers
27
+ });
28
+ const text = await res.text();
29
+ const body = text ? safeJson(text) : null;
30
+ if (!res.ok) {
31
+ const message = body && (body.message || body.error) || `Request failed (${res.status})`;
32
+ throw new MaltoApiError(
33
+ Array.isArray(message) ? message.join(", ") : String(message),
34
+ res.status
35
+ );
36
+ }
37
+ return body;
38
+ }
39
+ board() {
40
+ return this.request("/v1/sdk/board");
41
+ }
42
+ listFeedbacks() {
43
+ return this.request("/v1/sdk/feedbacks", { auth: true });
44
+ }
45
+ feedback(id) {
46
+ return this.request(`/v1/sdk/feedbacks/${id}`, {
47
+ auth: true
48
+ });
49
+ }
50
+ createFeedback(input) {
51
+ return this.request("/v1/sdk/feedbacks", {
52
+ method: "POST",
53
+ auth: true,
54
+ body: JSON.stringify(input)
55
+ });
56
+ }
57
+ vote(id) {
58
+ return this.request(`/v1/sdk/feedbacks/${id}/vote`, {
59
+ method: "POST",
60
+ auth: true
61
+ });
62
+ }
63
+ comments(id) {
64
+ return this.request(`/v1/sdk/feedbacks/${id}/comments`, {
65
+ auth: true
66
+ });
67
+ }
68
+ addComment(id, body, parentId) {
69
+ return this.request(`/v1/sdk/feedbacks/${id}/comments`, {
70
+ method: "POST",
71
+ auth: true,
72
+ body: JSON.stringify({ body, parentId })
73
+ });
74
+ }
75
+ roadmap() {
76
+ return this.request("/v1/sdk/roadmap");
77
+ }
78
+ releases() {
79
+ return this.request("/v1/sdk/releases");
80
+ }
81
+ startMagicLink(input) {
82
+ return this.request("/v1/sdk/auth/magic-link", {
83
+ method: "POST",
84
+ body: JSON.stringify(input)
85
+ });
86
+ }
87
+ verifyMagicLink(token) {
88
+ return this.request("/v1/sdk/auth/verify", {
89
+ method: "POST",
90
+ body: JSON.stringify({ token })
91
+ });
92
+ }
93
+ identify(payload) {
94
+ return this.request("/v1/sdk/auth/identify", {
95
+ method: "POST",
96
+ body: JSON.stringify(payload)
97
+ });
98
+ }
99
+ };
100
+ var MaltoApiError = class extends Error {
101
+ constructor(message, status) {
102
+ super(message);
103
+ this.status = status;
104
+ this.name = "MaltoApiError";
105
+ }
106
+ };
107
+ function safeJson(text) {
108
+ try {
109
+ return JSON.parse(text);
110
+ } catch {
111
+ return null;
112
+ }
113
+ }
114
+
115
+ // src/storage.ts
116
+ var KEY_PREFIX = "malto:session:";
117
+ function readSession(apiKeyPrefix) {
118
+ if (typeof window === "undefined") return null;
119
+ try {
120
+ const raw = window.localStorage.getItem(KEY_PREFIX + apiKeyPrefix);
121
+ if (!raw) return null;
122
+ const parsed = JSON.parse(raw);
123
+ if (!parsed?.token || !parsed?.expiresAt) return null;
124
+ if (new Date(parsed.expiresAt).getTime() < Date.now()) {
125
+ window.localStorage.removeItem(KEY_PREFIX + apiKeyPrefix);
126
+ return null;
127
+ }
128
+ return parsed;
129
+ } catch {
130
+ return null;
131
+ }
132
+ }
133
+ function writeSession(apiKeyPrefix, data) {
134
+ if (typeof window === "undefined") return;
135
+ try {
136
+ window.localStorage.setItem(
137
+ KEY_PREFIX + apiKeyPrefix,
138
+ JSON.stringify(data)
139
+ );
140
+ } catch {
141
+ }
142
+ }
143
+ function clearSession(apiKeyPrefix) {
144
+ if (typeof window === "undefined") return;
145
+ try {
146
+ window.localStorage.removeItem(KEY_PREFIX + apiKeyPrefix);
147
+ } catch {
148
+ }
149
+ }
150
+
151
+ // src/styles.ts
152
+ var STYLE_ID = "malto-sdk-styles";
153
+ var CUSTOM_STYLE_ID = "malto-sdk-styles-custom";
154
+ function injectStyles(opts) {
155
+ if (typeof document === "undefined") return;
156
+ const css = buildCss(opts);
157
+ upsert(STYLE_ID, css);
158
+ if (opts.customCss) {
159
+ upsert(CUSTOM_STYLE_ID, sanitizeCss(opts.customCss));
160
+ } else {
161
+ document.getElementById(CUSTOM_STYLE_ID)?.remove();
162
+ }
163
+ }
164
+ function sanitizeCss(css) {
165
+ return css.replace(/<\/style/gi, "<\\/style").replace(/<\/script/gi, "<\\/script").replace(/expression\s*\(/gi, "expr\\(").replace(/javascript\s*:/gi, "javascript\\:");
166
+ }
167
+ function upsert(id, css) {
168
+ const existing = document.getElementById(id);
169
+ if (existing) {
170
+ existing.textContent = css;
171
+ return;
172
+ }
173
+ const el = document.createElement("style");
174
+ el.id = id;
175
+ el.textContent = css;
176
+ document.head.appendChild(el);
177
+ }
178
+ function buildCss(opts) {
179
+ const primary = opts.primary;
180
+ const tones = derivePalette(primary);
181
+ const radiusScale = opts.radius === "sm" ? "10px" : opts.radius === "lg" ? "22px" : "16px";
182
+ const appearance = opts.appearance ?? "auto";
183
+ const overrides = opts.cssVars ?? {};
184
+ return `
185
+ :where(.malto-root, .malto-root *) {
186
+ box-sizing: border-box;
187
+ }
188
+ .malto-root {
189
+ --malto-primary: ${overrides.primary ?? primary};
190
+ --malto-primary-contrast: ${overrides.primaryContrast ?? tones.contrast};
191
+ --malto-primary-50: ${tones.tint50};
192
+ --malto-primary-100: ${tones.tint100};
193
+ --malto-primary-200: ${tones.tint200};
194
+ --malto-primary-600: ${tones.shade600};
195
+ --malto-primary-rgb: ${tones.rgb};
196
+ --malto-surface: ${overrides.surface ?? "#ffffff"};
197
+ --malto-surface-muted: ${overrides.surfaceMuted ?? "#fbfaff"};
198
+ --malto-surface-elevated: rgba(255,255,255,0.85);
199
+ --malto-border: ${overrides.border ?? "rgba(15, 13, 35, 0.06)"};
200
+ --malto-border-strong: rgba(15, 13, 35, 0.1);
201
+ --malto-text: ${overrides.text ?? "#0f0d23"};
202
+ --malto-text-muted: ${overrides.textMuted ?? "#5b5871"};
203
+ --malto-text-subtle: rgba(15, 13, 35, 0.45);
204
+ --malto-radius: ${overrides.radius ?? radiusScale};
205
+ --malto-radius-sm: 10px;
206
+ --malto-radius-pill: 999px;
207
+ --malto-shadow-sm: ${overrides.shadow ?? "0 4px 12px -4px rgba(15, 13, 35, 0.08)"};
208
+ --malto-shadow-md: 0 12px 32px -8px rgba(15, 13, 35, 0.16);
209
+ --malto-shadow-lg: 0 30px 60px -20px rgba(15, 13, 35, 0.28);
210
+ --malto-shadow-glow: 0 12px 24px -8px rgba(var(--malto-primary-rgb), 0.45);
211
+ --malto-font: ${overrides.font ?? "-apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', Roboto, system-ui, sans-serif"};
212
+ --malto-ease-spring: cubic-bezier(0.34, 1.4, 0.5, 1);
213
+ --malto-ease-smooth: cubic-bezier(0.32, 0.72, 0, 1);
214
+ }
215
+
216
+ ${appearance === "dark" ? darkOverrides() : ""}
217
+ ${appearance === "auto" ? `@media (prefers-color-scheme: dark) { ${darkOverrides()} }` : ""}
218
+
219
+ .malto-root, .malto-root *, .malto-root *::before, .malto-root *::after {
220
+ font-family: var(--malto-font);
221
+ -webkit-font-smoothing: antialiased;
222
+ -moz-osx-font-smoothing: grayscale;
223
+ }
224
+ .malto-root *::-webkit-scrollbar { width: 8px; height: 8px; }
225
+ .malto-root *::-webkit-scrollbar-track { background: transparent; }
226
+ .malto-root *::-webkit-scrollbar-thumb {
227
+ background: rgba(15, 13, 35, 0.12);
228
+ border-radius: 999px;
229
+ }
230
+ .malto-root *::-webkit-scrollbar-thumb:hover {
231
+ background: rgba(15, 13, 35, 0.22);
232
+ }
233
+
234
+ /* \u2500\u2500\u2500\u2500\u2500\u2500 Bubble (floating action button) \u2500\u2500\u2500\u2500\u2500\u2500 */
235
+ .malto-bubble {
236
+ position: fixed;
237
+ width: 56px; height: 56px;
238
+ border-radius: 999px;
239
+ background: var(--malto-primary);
240
+ color: var(--malto-primary-contrast);
241
+ display: flex; align-items: center; justify-content: center;
242
+ cursor: pointer; border: none;
243
+ box-shadow: var(--malto-shadow-glow);
244
+ z-index: var(--malto-z, 2147483600);
245
+ transition: transform .35s var(--malto-ease-spring), box-shadow .25s ease;
246
+ overflow: hidden;
247
+ }
248
+ .malto-bubble::before {
249
+ content: "";
250
+ position: absolute; inset: 0;
251
+ border-radius: inherit;
252
+ background: linear-gradient(135deg, rgba(255,255,255,0.18), transparent 50%);
253
+ pointer-events: none;
254
+ }
255
+ .malto-bubble::after {
256
+ content: "";
257
+ position: absolute; inset: -4px;
258
+ border-radius: inherit;
259
+ background: var(--malto-primary);
260
+ opacity: 0.25;
261
+ filter: blur(12px);
262
+ z-index: -1;
263
+ transition: opacity .25s ease;
264
+ }
265
+ .malto-bubble:hover { transform: scale(1.06) translateY(-2px); }
266
+ .malto-bubble:hover::after { opacity: 0.45; }
267
+ .malto-bubble:active { transform: scale(0.96); }
268
+ .malto-bubble svg { width: 24px; height: 24px; position: relative; z-index: 1; }
269
+
270
+ .malto-trigger {
271
+ display: inline-flex; align-items: center; gap: 8px;
272
+ padding: 9px 18px;
273
+ border-radius: var(--malto-radius-pill);
274
+ background: var(--malto-primary);
275
+ color: var(--malto-primary-contrast);
276
+ border: none; cursor: pointer;
277
+ font-size: 13px; font-weight: 600; letter-spacing: -0.01em;
278
+ box-shadow: var(--malto-shadow-glow);
279
+ transition: transform .25s var(--malto-ease-spring), box-shadow .2s ease;
280
+ }
281
+ .malto-trigger:hover { transform: translateY(-1px); }
282
+ .malto-trigger:active { transform: scale(0.97); }
283
+ .malto-trigger svg { width: 14px; height: 14px; }
284
+
285
+ /* \u2500\u2500\u2500\u2500\u2500\u2500 Overlay + modal \u2500\u2500\u2500\u2500\u2500\u2500 */
286
+ .malto-overlay {
287
+ position: fixed; inset: 0;
288
+ background: rgba(15, 13, 35, 0.32);
289
+ backdrop-filter: blur(6px) saturate(140%);
290
+ -webkit-backdrop-filter: blur(6px) saturate(140%);
291
+ z-index: var(--malto-z, 2147483600);
292
+ display: flex; align-items: flex-end; justify-content: center;
293
+ padding: 0;
294
+ animation: malto-overlay-in .25s var(--malto-ease-smooth);
295
+ }
296
+ @media (min-width: 640px) {
297
+ .malto-overlay { align-items: center; padding: 24px; }
298
+ }
299
+
300
+ .malto-modal {
301
+ background: var(--malto-surface);
302
+ width: 100%;
303
+ max-width: 460px;
304
+ max-height: 92vh;
305
+ border-radius: var(--malto-radius) var(--malto-radius) 0 0;
306
+ box-shadow: var(--malto-shadow-lg);
307
+ display: flex; flex-direction: column;
308
+ overflow: hidden;
309
+ animation: malto-modal-in .35s var(--malto-ease-spring);
310
+ position: relative;
311
+ }
312
+ @media (min-width: 640px) {
313
+ .malto-modal { border-radius: var(--malto-radius); }
314
+ }
315
+
316
+ .malto-inline {
317
+ width: 100%;
318
+ max-width: 480px;
319
+ background: var(--malto-surface);
320
+ border: 1px solid var(--malto-border);
321
+ border-radius: var(--malto-radius);
322
+ box-shadow: var(--malto-shadow-sm);
323
+ overflow: hidden;
324
+ display: flex; flex-direction: column;
325
+ max-height: 640px;
326
+ }
327
+
328
+ /* \u2500\u2500\u2500\u2500\u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500 */
329
+ .malto-header {
330
+ display: flex; align-items: center; justify-content: space-between;
331
+ padding: 18px 22px 14px;
332
+ position: relative;
333
+ flex: none;
334
+ }
335
+ .malto-header h3 {
336
+ margin: 0;
337
+ font-size: 16px; font-weight: 600;
338
+ color: var(--malto-text);
339
+ letter-spacing: -0.015em;
340
+ }
341
+ .malto-header-meta {
342
+ display: flex; align-items: center; gap: 10px;
343
+ }
344
+ .malto-board-logo {
345
+ width: 28px; height: 28px;
346
+ border-radius: 8px;
347
+ background: var(--malto-primary-100);
348
+ display: flex; align-items: center; justify-content: center;
349
+ color: var(--malto-primary);
350
+ font-size: 12px; font-weight: 700;
351
+ overflow: hidden;
352
+ object-fit: cover;
353
+ }
354
+ .malto-board-logo img { width: 100%; height: 100%; object-fit: cover; }
355
+ .malto-close {
356
+ width: 30px; height: 30px;
357
+ border-radius: 8px;
358
+ background: transparent; border: none; cursor: pointer;
359
+ color: var(--malto-text-muted);
360
+ font-size: 18px; line-height: 1;
361
+ display: flex; align-items: center; justify-content: center;
362
+ transition: background .15s ease, color .15s ease;
363
+ }
364
+ .malto-close:hover {
365
+ background: var(--malto-primary-50);
366
+ color: var(--malto-text);
367
+ }
368
+
369
+ /* \u2500\u2500\u2500\u2500\u2500\u2500 Tabs \u2500\u2500\u2500\u2500\u2500\u2500 */
370
+ .malto-tabs {
371
+ display: flex;
372
+ padding: 4px;
373
+ margin: 0 18px 14px;
374
+ background: var(--malto-primary-50);
375
+ border-radius: var(--malto-radius-sm);
376
+ flex: none;
377
+ }
378
+ .malto-tab {
379
+ flex: 1;
380
+ padding: 8px 10px;
381
+ background: transparent;
382
+ border: none; cursor: pointer;
383
+ font-size: 12px; font-weight: 600;
384
+ color: var(--malto-text-muted);
385
+ border-radius: 8px;
386
+ transition: all .25s var(--malto-ease-smooth);
387
+ letter-spacing: -0.005em;
388
+ }
389
+ .malto-tab:hover { color: var(--malto-text); }
390
+ .malto-tab[aria-selected="true"] {
391
+ background: var(--malto-surface);
392
+ color: var(--malto-primary);
393
+ box-shadow: var(--malto-shadow-sm);
394
+ }
395
+
396
+ /* \u2500\u2500\u2500\u2500\u2500\u2500 Body \u2500\u2500\u2500\u2500\u2500\u2500 */
397
+ .malto-body {
398
+ flex: 1;
399
+ overflow-y: auto;
400
+ padding: 4px 18px 18px;
401
+ background: var(--malto-surface);
402
+ scroll-behavior: smooth;
403
+ }
404
+ .malto-body > * {
405
+ animation: malto-fade-in .35s var(--malto-ease-smooth);
406
+ }
407
+ .malto-list {
408
+ display: flex; flex-direction: column;
409
+ gap: 8px;
410
+ }
411
+
412
+ /* \u2500\u2500\u2500\u2500\u2500\u2500 Feedback row \u2500\u2500\u2500\u2500\u2500\u2500 */
413
+ .malto-item {
414
+ display: flex; gap: 12px;
415
+ padding: 12px;
416
+ border-radius: 12px;
417
+ background: var(--malto-surface);
418
+ border: 1px solid var(--malto-border);
419
+ transition: all .25s var(--malto-ease-smooth);
420
+ position: relative;
421
+ cursor: pointer;
422
+ }
423
+ .malto-item:hover {
424
+ border-color: var(--malto-border-strong);
425
+ transform: translateY(-1px);
426
+ box-shadow: var(--malto-shadow-sm);
427
+ }
428
+ .malto-vote {
429
+ display: flex; flex-direction: column;
430
+ align-items: center; justify-content: center;
431
+ min-width: 44px; padding: 6px 4px;
432
+ border-radius: 10px;
433
+ border: 1px solid var(--malto-border);
434
+ background: var(--malto-surface-muted);
435
+ cursor: pointer;
436
+ font-size: 11px; color: var(--malto-text-muted);
437
+ transition: all .2s var(--malto-ease-spring);
438
+ user-select: none;
439
+ }
440
+ .malto-vote:hover {
441
+ border-color: var(--malto-primary);
442
+ color: var(--malto-primary);
443
+ transform: translateY(-1px);
444
+ }
445
+ .malto-vote:active { transform: scale(0.92); }
446
+ .malto-vote[aria-pressed="true"] {
447
+ border-color: var(--malto-primary);
448
+ background: var(--malto-primary);
449
+ color: var(--malto-primary-contrast);
450
+ box-shadow: 0 4px 12px -4px rgba(var(--malto-primary-rgb), 0.45);
451
+ }
452
+ .malto-vote-arrow {
453
+ font-size: 11px;
454
+ transition: transform .3s var(--malto-ease-spring);
455
+ }
456
+ .malto-vote:hover .malto-vote-arrow { transform: translateY(-2px); }
457
+ .malto-vote-count {
458
+ font-size: 13px; font-weight: 700;
459
+ letter-spacing: -0.01em;
460
+ margin-top: 1px;
461
+ }
462
+ .malto-item-content { flex: 1; min-width: 0; }
463
+ .malto-item h4 {
464
+ margin: 0 0 3px;
465
+ font-size: 14px; font-weight: 600;
466
+ color: var(--malto-text);
467
+ letter-spacing: -0.01em;
468
+ line-height: 1.3;
469
+ }
470
+ .malto-item p {
471
+ margin: 0;
472
+ font-size: 12.5px;
473
+ color: var(--malto-text-muted);
474
+ line-height: 1.45;
475
+ display: -webkit-box;
476
+ -webkit-line-clamp: 2;
477
+ -webkit-box-orient: vertical;
478
+ overflow: hidden;
479
+ }
480
+ .malto-item-meta {
481
+ display: flex; align-items: center; gap: 8px;
482
+ margin-top: 6px;
483
+ font-size: 11px;
484
+ color: var(--malto-text-subtle);
485
+ }
486
+ .malto-item-meta-dot {
487
+ width: 3px; height: 3px;
488
+ border-radius: 999px;
489
+ background: currentColor;
490
+ opacity: 0.4;
491
+ }
492
+
493
+ /* \u2500\u2500\u2500\u2500\u2500\u2500 Status pills \u2500\u2500\u2500\u2500\u2500\u2500 */
494
+ .malto-status {
495
+ display: inline-flex; align-items: center; gap: 5px;
496
+ padding: 2px 8px;
497
+ border-radius: 999px;
498
+ font-size: 10px; font-weight: 600;
499
+ text-transform: uppercase; letter-spacing: 0.04em;
500
+ line-height: 1.4;
501
+ }
502
+ .malto-status-dot {
503
+ width: 5px; height: 5px;
504
+ border-radius: 999px;
505
+ background: currentColor;
506
+ }
507
+ .malto-status-PLANNED { background: rgba(99, 102, 241, 0.1); color: #4f46e5; }
508
+ .malto-status-IN_PROGRESS { background: rgba(245, 158, 11, 0.1); color: #b45309; }
509
+ .malto-status-COMPLETED { background: rgba(34, 197, 94, 0.1); color: #15803d; }
510
+ .malto-status-DECLINED { background: rgba(244, 63, 94, 0.1); color: #be123c; }
511
+ .malto-status-UNDER_REVIEW { background: rgba(168, 85, 247, 0.1); color: #7e22ce; }
512
+ .malto-status-BACKLOG { background: rgba(100, 116, 139, 0.1); color: #475569; }
513
+
514
+ /* \u2500\u2500\u2500\u2500\u2500\u2500 Forms \u2500\u2500\u2500\u2500\u2500\u2500 */
515
+ .malto-form {
516
+ display: flex; flex-direction: column;
517
+ gap: 10px;
518
+ }
519
+ .malto-input, .malto-textarea {
520
+ width: 100%;
521
+ padding: 11px 14px;
522
+ border: 1px solid var(--malto-border-strong);
523
+ border-radius: 12px;
524
+ font-size: 13.5px;
525
+ color: var(--malto-text);
526
+ background: var(--malto-surface);
527
+ outline: none;
528
+ transition: all .2s ease;
529
+ font-family: inherit;
530
+ }
531
+ .malto-input::placeholder, .malto-textarea::placeholder {
532
+ color: var(--malto-text-subtle);
533
+ }
534
+ .malto-input:focus, .malto-textarea:focus {
535
+ border-color: var(--malto-primary);
536
+ box-shadow: 0 0 0 4px rgba(var(--malto-primary-rgb), 0.1);
537
+ }
538
+ .malto-textarea {
539
+ min-height: 110px;
540
+ resize: vertical;
541
+ line-height: 1.5;
542
+ }
543
+ .malto-button {
544
+ padding: 11px 18px;
545
+ border-radius: 12px;
546
+ background: var(--malto-primary);
547
+ color: var(--malto-primary-contrast);
548
+ border: none; cursor: pointer;
549
+ font-size: 13px; font-weight: 600;
550
+ letter-spacing: -0.005em;
551
+ box-shadow: var(--malto-shadow-glow);
552
+ transition: transform .2s var(--malto-ease-spring), box-shadow .2s ease, opacity .2s ease;
553
+ display: inline-flex; align-items: center; justify-content: center; gap: 6px;
554
+ }
555
+ .malto-button:hover { transform: translateY(-1px); }
556
+ .malto-button:active { transform: scale(0.97); }
557
+ .malto-button[disabled] { opacity: .5; cursor: not-allowed; transform: none; }
558
+ .malto-secondary {
559
+ padding: 9px 14px;
560
+ border-radius: 10px;
561
+ background: transparent;
562
+ color: var(--malto-text);
563
+ border: 1px solid var(--malto-border-strong);
564
+ cursor: pointer;
565
+ font-size: 13px; font-weight: 500;
566
+ transition: all .2s ease;
567
+ }
568
+ .malto-secondary:hover {
569
+ border-color: var(--malto-primary);
570
+ color: var(--malto-primary);
571
+ background: var(--malto-primary-50);
572
+ }
573
+ .malto-link {
574
+ color: var(--malto-primary);
575
+ background: none; border: none; padding: 0;
576
+ font-size: 13px; font-weight: 600;
577
+ cursor: pointer;
578
+ text-decoration: underline;
579
+ text-underline-offset: 3px;
580
+ text-decoration-color: rgba(var(--malto-primary-rgb), 0.3);
581
+ transition: text-decoration-color .2s ease;
582
+ }
583
+ .malto-link:hover {
584
+ text-decoration-color: var(--malto-primary);
585
+ }
586
+
587
+ /* \u2500\u2500\u2500\u2500\u2500\u2500 Empty / loading \u2500\u2500\u2500\u2500\u2500\u2500 */
588
+ .malto-empty {
589
+ padding: 40px 18px;
590
+ text-align: center;
591
+ display: flex; flex-direction: column; align-items: center; gap: 8px;
592
+ }
593
+ .malto-empty-icon {
594
+ width: 44px; height: 44px;
595
+ border-radius: 14px;
596
+ background: var(--malto-primary-50);
597
+ color: var(--malto-primary);
598
+ display: flex; align-items: center; justify-content: center;
599
+ margin-bottom: 4px;
600
+ }
601
+ .malto-empty-title {
602
+ font-size: 14px; font-weight: 600;
603
+ color: var(--malto-text);
604
+ }
605
+ .malto-empty-desc {
606
+ font-size: 12.5px;
607
+ color: var(--malto-text-muted);
608
+ max-width: 240px;
609
+ line-height: 1.5;
610
+ }
611
+ .malto-skeleton {
612
+ background: linear-gradient(90deg,
613
+ var(--malto-primary-50) 0%,
614
+ var(--malto-primary-100) 50%,
615
+ var(--malto-primary-50) 100%);
616
+ background-size: 200% 100%;
617
+ border-radius: 8px;
618
+ animation: malto-shimmer 1.4s linear infinite;
619
+ }
620
+ .malto-skeleton-row {
621
+ padding: 12px;
622
+ border: 1px solid var(--malto-border);
623
+ border-radius: 12px;
624
+ display: flex; gap: 12px;
625
+ margin-bottom: 8px;
626
+ }
627
+ .malto-error {
628
+ padding: 10px 12px;
629
+ margin-bottom: 10px;
630
+ background: rgba(244, 63, 94, 0.08);
631
+ color: #be123c;
632
+ border-radius: 10px;
633
+ font-size: 12.5px;
634
+ border-left: 3px solid #f43f5e;
635
+ }
636
+
637
+ /* \u2500\u2500\u2500\u2500\u2500\u2500 Roadmap \u2500\u2500\u2500\u2500\u2500\u2500 */
638
+ .malto-roadmap-col { margin-bottom: 18px; }
639
+ .malto-roadmap-col h5 {
640
+ display: flex; align-items: center; gap: 8px;
641
+ margin: 0 0 8px;
642
+ font-size: 11px;
643
+ text-transform: uppercase;
644
+ letter-spacing: 0.06em;
645
+ color: var(--malto-text-muted);
646
+ font-weight: 600;
647
+ }
648
+ .malto-roadmap-count {
649
+ background: var(--malto-primary-50);
650
+ color: var(--malto-primary);
651
+ border-radius: 999px;
652
+ padding: 1px 7px;
653
+ font-size: 10px; font-weight: 700;
654
+ }
655
+
656
+ /* \u2500\u2500\u2500\u2500\u2500\u2500 Releases / changelog \u2500\u2500\u2500\u2500\u2500\u2500 */
657
+ .malto-release {
658
+ padding: 14px;
659
+ border: 1px solid var(--malto-border);
660
+ border-radius: 12px;
661
+ margin-bottom: 8px;
662
+ transition: all .2s ease;
663
+ }
664
+ .malto-release:hover {
665
+ border-color: var(--malto-border-strong);
666
+ box-shadow: var(--malto-shadow-sm);
667
+ }
668
+ .malto-release h4 {
669
+ margin: 0 0 4px;
670
+ font-size: 13.5px; font-weight: 600;
671
+ color: var(--malto-text);
672
+ letter-spacing: -0.01em;
673
+ }
674
+ .malto-release p {
675
+ margin: 0;
676
+ font-size: 12.5px;
677
+ color: var(--malto-text-muted);
678
+ line-height: 1.5;
679
+ white-space: pre-wrap;
680
+ }
681
+ .malto-release-date {
682
+ font-size: 10.5px;
683
+ color: var(--malto-text-subtle);
684
+ text-transform: uppercase;
685
+ letter-spacing: 0.05em;
686
+ margin-bottom: 6px;
687
+ display: block;
688
+ }
689
+
690
+ /* \u2500\u2500\u2500\u2500\u2500\u2500 Comments \u2500\u2500\u2500\u2500\u2500\u2500 */
691
+ .malto-comment {
692
+ padding: 10px 0;
693
+ display: flex; gap: 10px;
694
+ border-top: 1px solid var(--malto-border);
695
+ }
696
+ .malto-comment:first-child { border-top: none; }
697
+ .malto-avatar {
698
+ width: 28px; height: 28px;
699
+ border-radius: 999px;
700
+ background: var(--malto-primary-100);
701
+ color: var(--malto-primary);
702
+ flex: none;
703
+ display: flex; align-items: center; justify-content: center;
704
+ font-size: 11px; font-weight: 700;
705
+ overflow: hidden;
706
+ }
707
+ .malto-avatar img { width: 100%; height: 100%; object-fit: cover; }
708
+ .malto-comment-author {
709
+ font-size: 12.5px; font-weight: 600;
710
+ color: var(--malto-text);
711
+ }
712
+ .malto-comment-time {
713
+ font-size: 11px;
714
+ color: var(--malto-text-subtle);
715
+ margin-left: 6px;
716
+ }
717
+ .malto-comment-body {
718
+ font-size: 13px;
719
+ color: var(--malto-text);
720
+ line-height: 1.5;
721
+ margin-top: 2px;
722
+ white-space: pre-wrap;
723
+ }
724
+
725
+ /* \u2500\u2500\u2500\u2500\u2500\u2500 Footer banner (sign-in CTA) \u2500\u2500\u2500\u2500\u2500\u2500 */
726
+ .malto-footer-banner {
727
+ flex: none;
728
+ padding: 12px 18px;
729
+ background: var(--malto-surface-muted);
730
+ border-top: 1px solid var(--malto-border);
731
+ font-size: 12px;
732
+ color: var(--malto-text-muted);
733
+ display: flex; align-items: center; justify-content: space-between;
734
+ gap: 10px;
735
+ }
736
+ .malto-footer-banner strong { color: var(--malto-text); font-weight: 600; }
737
+
738
+ .malto-back {
739
+ display: inline-flex; align-items: center; gap: 4px;
740
+ padding: 6px 10px;
741
+ background: transparent;
742
+ border: none; cursor: pointer;
743
+ font-size: 12px; font-weight: 600;
744
+ color: var(--malto-text-muted);
745
+ border-radius: 8px;
746
+ transition: all .15s ease;
747
+ margin-bottom: 10px;
748
+ }
749
+ .malto-back:hover {
750
+ color: var(--malto-text);
751
+ background: var(--malto-primary-50);
752
+ }
753
+
754
+ /* \u2500\u2500\u2500\u2500\u2500\u2500 Animations \u2500\u2500\u2500\u2500\u2500\u2500 */
755
+ @keyframes malto-overlay-in {
756
+ from { opacity: 0; backdrop-filter: blur(0); }
757
+ to { opacity: 1; }
758
+ }
759
+ @keyframes malto-modal-in {
760
+ 0% { opacity: 0; transform: translateY(20px) scale(0.96); }
761
+ 100% { opacity: 1; transform: translateY(0) scale(1); }
762
+ }
763
+ @keyframes malto-fade-in {
764
+ from { opacity: 0; transform: translateY(4px); }
765
+ to { opacity: 1; transform: translateY(0); }
766
+ }
767
+ @keyframes malto-shimmer {
768
+ 0% { background-position: 200% 0; }
769
+ 100% { background-position: -200% 0; }
770
+ }
771
+ @media (prefers-reduced-motion: reduce) {
772
+ .malto-root *,
773
+ .malto-root *::before,
774
+ .malto-root *::after {
775
+ animation-duration: 0.01ms !important;
776
+ transition-duration: 0.01ms !important;
777
+ }
778
+ }
779
+ `;
780
+ }
781
+ function darkOverrides() {
782
+ return `
783
+ .malto-root {
784
+ --malto-surface: #131127;
785
+ --malto-surface-muted: #1a1834;
786
+ --malto-border: rgba(255, 255, 255, 0.08);
787
+ --malto-border-strong: rgba(255, 255, 255, 0.16);
788
+ --malto-text: #e6e3f5;
789
+ --malto-text-muted: #a09cc7;
790
+ --malto-text-subtle: rgba(230, 227, 245, 0.5);
791
+ }
792
+ `;
793
+ }
794
+ function derivePalette(hex) {
795
+ const { r, g, b } = parseHex(hex);
796
+ return {
797
+ rgb: `${r}, ${g}, ${b}`,
798
+ contrast: contrastColor(r, g, b),
799
+ tint50: rgba(r, g, b, 0.06),
800
+ tint100: rgba(r, g, b, 0.12),
801
+ tint200: rgba(r, g, b, 0.18),
802
+ shade600: shade(r, g, b, -0.18)
803
+ };
804
+ }
805
+ function parseHex(input) {
806
+ let h = input.trim().replace(/^#/, "");
807
+ if (h.length === 3)
808
+ h = h.split("").map((c) => c + c).join("");
809
+ if (!/^[0-9a-fA-F]{6}$/.test(h)) return { r: 69, g: 47, b: 223 };
810
+ return {
811
+ r: parseInt(h.slice(0, 2), 16),
812
+ g: parseInt(h.slice(2, 4), 16),
813
+ b: parseInt(h.slice(4, 6), 16)
814
+ };
815
+ }
816
+ function contrastColor(r, g, b) {
817
+ const l = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
818
+ return l > 0.65 ? "#0f0d23" : "#ffffff";
819
+ }
820
+ function rgba(r, g, b, a, _fallback) {
821
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
822
+ }
823
+ function shade(r, g, b, amt) {
824
+ const f = (c) => Math.max(0, Math.min(255, Math.round(c + 255 * amt)));
825
+ const toHex = (c) => f(c).toString(16).padStart(2, "0");
826
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
827
+ }
828
+
829
+ // src/widget.ts
830
+ var POSITIONS_TO_CSS = {
831
+ "bottom-right": { right: "24px", bottom: "24px" },
832
+ "bottom-left": { left: "24px", bottom: "24px" },
833
+ "top-right": { right: "24px", top: "24px" },
834
+ "top-left": { left: "24px", top: "24px" }
835
+ };
836
+ var MaltoWidget = class {
837
+ constructor(config) {
838
+ this.resolvedMode = "bubble";
839
+ this.root = null;
840
+ this.container = null;
841
+ this.trigger = null;
842
+ this.board = null;
843
+ if (!config.apiKey) throw new Error("Malto: apiKey is required");
844
+ this.apiKeyPrefix = config.apiKey.slice(0, 16);
845
+ this.config = {
846
+ apiKey: config.apiKey,
847
+ apiUrl: config.apiUrl ?? "https://api.malto.io",
848
+ mode: config.mode ?? "auto",
849
+ position: config.position ?? "bottom-right",
850
+ triggerLabel: config.triggerLabel ?? "Feedback",
851
+ primaryColor: config.primaryColor,
852
+ cssVars: config.cssVars,
853
+ customCss: config.customCss,
854
+ radius: config.radius ?? "md",
855
+ appearance: config.appearance ?? "auto",
856
+ zIndex: config.zIndex ?? 2147483600,
857
+ views: config.views,
858
+ identify: config.identify,
859
+ target: config.target,
860
+ onReady: config.onReady,
861
+ onError: config.onError
862
+ };
863
+ const session = readSession(this.apiKeyPrefix);
864
+ this.state = {
865
+ open: false,
866
+ view: "list",
867
+ loading: false,
868
+ error: null,
869
+ feedbacks: [],
870
+ roadmap: {},
871
+ releases: [],
872
+ selectedFeedback: null,
873
+ comments: [],
874
+ authEmail: "",
875
+ authStatus: "idle",
876
+ email: session?.email ?? null,
877
+ name: session?.name ?? null
878
+ };
879
+ this.client = new MaltoClient({
880
+ apiKey: this.config.apiKey,
881
+ apiUrl: this.config.apiUrl,
882
+ getSessionToken: () => readSession(this.apiKeyPrefix)?.token ?? null
883
+ });
884
+ }
885
+ async mount() {
886
+ try {
887
+ const board = await this.client.board();
888
+ this.board = board;
889
+ const primary = this.config.primaryColor || board.widget.primaryColor || board.board.boardThemeColor || "#452fdf";
890
+ injectStyles({
891
+ primary,
892
+ cssVars: this.config.cssVars,
893
+ customCss: this.config.customCss,
894
+ radius: this.config.radius,
895
+ appearance: this.config.appearance
896
+ });
897
+ this.resolvedMode = this.config.mode === "auto" ? board.widget.defaultMode : this.config.mode;
898
+ if (this.config.identify) {
899
+ await this.runIdentify(this.config.identify);
900
+ }
901
+ this.renderHost(this.resolvedMode);
902
+ this.config.onReady?.();
903
+ } catch (err) {
904
+ this.config.onError?.(err);
905
+ }
906
+ }
907
+ unmount() {
908
+ this.root?.parentElement?.removeChild(this.root);
909
+ this.trigger?.parentElement?.removeChild(this.trigger);
910
+ this.root = null;
911
+ this.trigger = null;
912
+ this.container = null;
913
+ }
914
+ open() {
915
+ this.state.open = true;
916
+ this.render();
917
+ }
918
+ close() {
919
+ this.state.open = false;
920
+ this.render();
921
+ }
922
+ setIdentify(identify2) {
923
+ if (!identify2) {
924
+ clearSession(this.apiKeyPrefix);
925
+ this.state.email = null;
926
+ this.state.name = null;
927
+ this.render();
928
+ return;
929
+ }
930
+ void this.runIdentify(identify2);
931
+ }
932
+ async runIdentify(identify2) {
933
+ try {
934
+ const session = await this.client.identify(identify2);
935
+ writeSession(this.apiKeyPrefix, {
936
+ token: session.sessionToken,
937
+ expiresAt: session.expiresAt,
938
+ email: session.user.email,
939
+ name: session.user.name
940
+ });
941
+ this.state.email = session.user.email;
942
+ this.state.name = session.user.name;
943
+ this.render();
944
+ } catch (err) {
945
+ this.config.onError?.(err);
946
+ }
947
+ }
948
+ renderHost(mode) {
949
+ const root = document.createElement("div");
950
+ root.className = "malto-root";
951
+ root.style.setProperty("--malto-z", String(this.config.zIndex));
952
+ document.body.appendChild(root);
953
+ this.root = root;
954
+ if (mode === "inline") {
955
+ const target = this.resolveTarget();
956
+ if (!target) {
957
+ this.config.onError?.(new Error("Malto: inline target not found"));
958
+ return;
959
+ }
960
+ const container = document.createElement("div");
961
+ container.className = "malto-root malto-inline";
962
+ container.style.setProperty("--malto-z", String(this.config.zIndex));
963
+ target.appendChild(container);
964
+ this.container = container;
965
+ this.state.open = true;
966
+ this.render();
967
+ return;
968
+ }
969
+ if (mode === "bubble") {
970
+ const btn = this.buildBubble();
971
+ const pos = POSITIONS_TO_CSS[this.config.position];
972
+ Object.assign(btn.style, pos);
973
+ const wrapper = document.createElement("div");
974
+ wrapper.className = "malto-root";
975
+ wrapper.style.setProperty("--malto-z", String(this.config.zIndex));
976
+ wrapper.appendChild(btn);
977
+ document.body.appendChild(wrapper);
978
+ this.trigger = wrapper;
979
+ } else if (mode === "trigger") {
980
+ const target = this.resolveTarget();
981
+ if (!target) {
982
+ this.config.onError?.(new Error("Malto: trigger target not found"));
983
+ return;
984
+ }
985
+ const wrapper = document.createElement("span");
986
+ wrapper.className = "malto-root";
987
+ wrapper.style.setProperty("--malto-z", String(this.config.zIndex));
988
+ const btn = document.createElement("button");
989
+ btn.className = "malto-trigger";
990
+ btn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg><span>${escape(this.config.triggerLabel)}</span>`;
991
+ btn.addEventListener("click", () => this.open());
992
+ wrapper.appendChild(btn);
993
+ target.appendChild(wrapper);
994
+ this.trigger = wrapper;
995
+ } else if (mode === "modal") {
996
+ this.state.open = true;
997
+ }
998
+ this.render();
999
+ }
1000
+ buildBubble() {
1001
+ const btn = document.createElement("button");
1002
+ btn.className = "malto-bubble";
1003
+ btn.setAttribute("aria-label", this.config.triggerLabel);
1004
+ btn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>`;
1005
+ btn.addEventListener("click", () => {
1006
+ this.state.open ? this.close() : this.open();
1007
+ });
1008
+ return btn;
1009
+ }
1010
+ resolveTarget() {
1011
+ if (!this.config.target) return null;
1012
+ if (typeof this.config.target === "string") {
1013
+ return document.querySelector(this.config.target);
1014
+ }
1015
+ return this.config.target;
1016
+ }
1017
+ render() {
1018
+ if (!this.root) return;
1019
+ if (this.resolvedMode === "inline") {
1020
+ if (!this.container) return;
1021
+ this.container.innerHTML = "";
1022
+ this.container.appendChild(this.buildPanelBody());
1023
+ return;
1024
+ }
1025
+ this.root.innerHTML = "";
1026
+ if (!this.state.open) return;
1027
+ const overlay = document.createElement("div");
1028
+ overlay.className = "malto-overlay";
1029
+ overlay.addEventListener("click", (e) => {
1030
+ if (e.target === overlay) this.close();
1031
+ });
1032
+ const modal = document.createElement("div");
1033
+ modal.className = "malto-modal";
1034
+ modal.appendChild(this.buildPanelBody());
1035
+ overlay.appendChild(modal);
1036
+ this.root.appendChild(overlay);
1037
+ }
1038
+ buildPanelBody() {
1039
+ const wrap = document.createElement("div");
1040
+ wrap.style.display = "flex";
1041
+ wrap.style.flexDirection = "column";
1042
+ wrap.style.height = "100%";
1043
+ wrap.style.minHeight = "0";
1044
+ wrap.appendChild(this.buildHeader());
1045
+ wrap.appendChild(this.buildTabs());
1046
+ const body = document.createElement("div");
1047
+ body.className = "malto-body";
1048
+ body.appendChild(this.buildView());
1049
+ wrap.appendChild(body);
1050
+ if (this.state.view !== "auth" && !this.state.email) {
1051
+ const banner = document.createElement("div");
1052
+ banner.className = "malto-footer-banner";
1053
+ const txt = document.createElement("span");
1054
+ txt.innerHTML = `Sign in to vote, comment, or submit ideas.`;
1055
+ const link = document.createElement("button");
1056
+ link.className = "malto-link";
1057
+ link.textContent = "Sign in";
1058
+ link.addEventListener("click", () => this.go("auth"));
1059
+ banner.appendChild(txt);
1060
+ banner.appendChild(link);
1061
+ wrap.appendChild(banner);
1062
+ }
1063
+ return wrap;
1064
+ }
1065
+ buildHeader() {
1066
+ const header = document.createElement("div");
1067
+ header.className = "malto-header";
1068
+ const meta = document.createElement("div");
1069
+ meta.className = "malto-header-meta";
1070
+ const logo = document.createElement("div");
1071
+ logo.className = "malto-board-logo";
1072
+ const logoUrl = safeImageUrl(this.board?.board.companyLogoUrl);
1073
+ if (logoUrl) {
1074
+ const img = document.createElement("img");
1075
+ img.src = logoUrl;
1076
+ img.alt = "";
1077
+ img.referrerPolicy = "no-referrer";
1078
+ img.crossOrigin = "anonymous";
1079
+ logo.appendChild(img);
1080
+ } else {
1081
+ logo.textContent = (this.board?.board.companyName ?? this.board?.board.boardName ?? "M").charAt(0).toUpperCase();
1082
+ }
1083
+ const title = document.createElement("h3");
1084
+ title.textContent = this.board?.board.boardName ?? this.board?.board.companyName ?? "Feedback";
1085
+ meta.appendChild(logo);
1086
+ meta.appendChild(title);
1087
+ header.appendChild(meta);
1088
+ if (this.resolvedMode !== "inline") {
1089
+ const close2 = document.createElement("button");
1090
+ close2.className = "malto-close";
1091
+ close2.setAttribute("aria-label", "Close");
1092
+ close2.innerHTML = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
1093
+ close2.addEventListener("click", () => this.close());
1094
+ header.appendChild(close2);
1095
+ }
1096
+ return header;
1097
+ }
1098
+ buildTabs() {
1099
+ const tabs = document.createElement("div");
1100
+ tabs.className = "malto-tabs";
1101
+ const enabled = this.allowedViews();
1102
+ if (enabled.includes("list")) tabs.appendChild(this.tabBtn("Feed", "list"));
1103
+ if (enabled.includes("submit"))
1104
+ tabs.appendChild(this.tabBtn("New", "submit"));
1105
+ if (enabled.includes("roadmap"))
1106
+ tabs.appendChild(this.tabBtn("Roadmap", "roadmap"));
1107
+ if (enabled.includes("changelog"))
1108
+ tabs.appendChild(this.tabBtn("Updates", "changelog"));
1109
+ return tabs;
1110
+ }
1111
+ tabBtn(label, view) {
1112
+ const btn = document.createElement("button");
1113
+ btn.className = "malto-tab";
1114
+ btn.setAttribute(
1115
+ "aria-selected",
1116
+ this.state.view === view ? "true" : "false"
1117
+ );
1118
+ btn.textContent = label;
1119
+ btn.addEventListener("click", () => this.go(view));
1120
+ return btn;
1121
+ }
1122
+ go(view) {
1123
+ this.state.view = view;
1124
+ this.state.error = null;
1125
+ this.render();
1126
+ if (view === "list") void this.loadFeedbacks();
1127
+ if (view === "roadmap") void this.loadRoadmap();
1128
+ if (view === "changelog") void this.loadReleases();
1129
+ if (view === "detail" && this.state.selectedFeedback) {
1130
+ void this.loadComments(this.state.selectedFeedback.id);
1131
+ }
1132
+ }
1133
+ allowedViews() {
1134
+ const settings = this.board?.widget.enabledFeatures ?? [
1135
+ "list",
1136
+ "submit",
1137
+ "vote",
1138
+ "comment",
1139
+ "roadmap",
1140
+ "changelog"
1141
+ ];
1142
+ const requested = this.config.views ?? [
1143
+ "list",
1144
+ "submit",
1145
+ "roadmap",
1146
+ "changelog"
1147
+ ];
1148
+ return requested.filter((v) => settings.includes(v));
1149
+ }
1150
+ buildView() {
1151
+ const wrap = document.createElement("div");
1152
+ if (this.state.error) {
1153
+ const e = document.createElement("div");
1154
+ e.className = "malto-error";
1155
+ e.textContent = this.state.error;
1156
+ wrap.appendChild(e);
1157
+ }
1158
+ switch (this.state.view) {
1159
+ case "list":
1160
+ wrap.appendChild(this.viewList());
1161
+ if (this.state.feedbacks.length === 0 && !this.state.loading) {
1162
+ void this.loadFeedbacks();
1163
+ }
1164
+ break;
1165
+ case "submit":
1166
+ wrap.appendChild(this.viewSubmit());
1167
+ break;
1168
+ case "auth":
1169
+ wrap.appendChild(this.viewAuth());
1170
+ break;
1171
+ case "roadmap":
1172
+ wrap.appendChild(this.viewRoadmap());
1173
+ if (Object.keys(this.state.roadmap).length === 0 && !this.state.loading) {
1174
+ void this.loadRoadmap();
1175
+ }
1176
+ break;
1177
+ case "changelog":
1178
+ wrap.appendChild(this.viewChangelog());
1179
+ if (this.state.releases.length === 0 && !this.state.loading) {
1180
+ void this.loadReleases();
1181
+ }
1182
+ break;
1183
+ case "detail":
1184
+ wrap.appendChild(this.viewDetail());
1185
+ break;
1186
+ }
1187
+ return wrap;
1188
+ }
1189
+ viewList() {
1190
+ const wrap = document.createElement("div");
1191
+ if (this.state.loading) {
1192
+ wrap.appendChild(this.skeletonList(4));
1193
+ return wrap;
1194
+ }
1195
+ if (this.state.feedbacks.length === 0) {
1196
+ wrap.appendChild(
1197
+ this.emptyState(
1198
+ "\u{1F4A1}",
1199
+ "No requests yet",
1200
+ "Be the first to share an idea or report something."
1201
+ )
1202
+ );
1203
+ return wrap;
1204
+ }
1205
+ const list = document.createElement("div");
1206
+ list.className = "malto-list";
1207
+ for (const fb of this.state.feedbacks) {
1208
+ list.appendChild(this.feedbackRow(fb));
1209
+ }
1210
+ wrap.appendChild(list);
1211
+ return wrap;
1212
+ }
1213
+ feedbackRow(fb) {
1214
+ const row = document.createElement("div");
1215
+ row.className = "malto-item";
1216
+ const vote = document.createElement("button");
1217
+ vote.className = "malto-vote";
1218
+ vote.setAttribute("aria-pressed", fb.hasVoted ? "true" : "false");
1219
+ const arrow = document.createElement("span");
1220
+ arrow.className = "malto-vote-arrow";
1221
+ arrow.textContent = "\u25B2";
1222
+ const count = document.createElement("span");
1223
+ count.className = "malto-vote-count";
1224
+ count.textContent = formatCount(fb.votes ?? 0);
1225
+ vote.appendChild(arrow);
1226
+ vote.appendChild(count);
1227
+ vote.addEventListener("click", (e) => {
1228
+ e.stopPropagation();
1229
+ void this.handleVote(fb);
1230
+ });
1231
+ const content = document.createElement("div");
1232
+ content.className = "malto-item-content";
1233
+ const title = document.createElement("h4");
1234
+ title.textContent = fb.title;
1235
+ content.appendChild(title);
1236
+ if (fb.description) {
1237
+ const desc = document.createElement("p");
1238
+ desc.textContent = fb.description;
1239
+ content.appendChild(desc);
1240
+ }
1241
+ const meta = document.createElement("div");
1242
+ meta.className = "malto-item-meta";
1243
+ if (fb.status && fb.status !== "BACKLOG") {
1244
+ const pill = statusPill(fb.status);
1245
+ meta.appendChild(pill);
1246
+ }
1247
+ const time = document.createElement("span");
1248
+ time.textContent = relativeTime(fb.createdAt);
1249
+ meta.appendChild(time);
1250
+ content.appendChild(meta);
1251
+ row.appendChild(vote);
1252
+ row.appendChild(content);
1253
+ row.addEventListener("click", () => {
1254
+ this.state.selectedFeedback = fb;
1255
+ this.go("detail");
1256
+ });
1257
+ return row;
1258
+ }
1259
+ viewSubmit() {
1260
+ const form = document.createElement("form");
1261
+ form.className = "malto-form";
1262
+ const titleInput = document.createElement("input");
1263
+ titleInput.className = "malto-input";
1264
+ titleInput.required = true;
1265
+ titleInput.maxLength = 200;
1266
+ titleInput.placeholder = "What would you like?";
1267
+ const descInput = document.createElement("textarea");
1268
+ descInput.className = "malto-textarea";
1269
+ descInput.maxLength = 5e3;
1270
+ descInput.placeholder = "Add some context (optional)";
1271
+ const submit = document.createElement("button");
1272
+ submit.className = "malto-button";
1273
+ submit.textContent = "Submit feedback";
1274
+ submit.type = "submit";
1275
+ form.appendChild(titleInput);
1276
+ form.appendChild(descInput);
1277
+ form.appendChild(submit);
1278
+ form.addEventListener("submit", async (e) => {
1279
+ e.preventDefault();
1280
+ if (!this.state.email) {
1281
+ this.go("auth");
1282
+ return;
1283
+ }
1284
+ submit.setAttribute("disabled", "true");
1285
+ try {
1286
+ await this.client.createFeedback({
1287
+ title: titleInput.value.trim(),
1288
+ description: descInput.value.trim() || void 0
1289
+ });
1290
+ titleInput.value = "";
1291
+ descInput.value = "";
1292
+ await this.loadFeedbacks();
1293
+ this.go("list");
1294
+ } catch (err) {
1295
+ this.state.error = err.message;
1296
+ submit.removeAttribute("disabled");
1297
+ this.render();
1298
+ }
1299
+ });
1300
+ return form;
1301
+ }
1302
+ viewAuth() {
1303
+ const wrap = document.createElement("div");
1304
+ if (this.state.email) {
1305
+ const p = document.createElement("p");
1306
+ p.style.fontSize = "13px";
1307
+ p.style.color = "var(--malto-text)";
1308
+ p.style.marginBottom = "10px";
1309
+ p.innerHTML = `Signed in as <strong>${escape(this.state.email)}</strong>`;
1310
+ const out = document.createElement("button");
1311
+ out.className = "malto-secondary";
1312
+ out.textContent = "Sign out";
1313
+ out.addEventListener("click", () => {
1314
+ clearSession(this.apiKeyPrefix);
1315
+ this.state.email = null;
1316
+ this.state.name = null;
1317
+ this.go("list");
1318
+ });
1319
+ wrap.appendChild(p);
1320
+ wrap.appendChild(out);
1321
+ return wrap;
1322
+ }
1323
+ if (this.state.authStatus === "sent") {
1324
+ const sent = document.createElement("p");
1325
+ sent.style.fontSize = "13px";
1326
+ sent.style.color = "var(--malto-text)";
1327
+ sent.style.marginBottom = "12px";
1328
+ sent.style.lineHeight = "1.5";
1329
+ sent.innerHTML = `We sent a sign-in link to <strong>${escape(this.state.authEmail)}</strong>. Open the email and paste the token below.`;
1330
+ const tokenInput = document.createElement("input");
1331
+ tokenInput.className = "malto-input";
1332
+ tokenInput.placeholder = "Paste token from email";
1333
+ const verifyBtn = document.createElement("button");
1334
+ verifyBtn.className = "malto-button";
1335
+ verifyBtn.style.marginTop = "8px";
1336
+ verifyBtn.textContent = "Verify";
1337
+ verifyBtn.addEventListener("click", async () => {
1338
+ verifyBtn.setAttribute("disabled", "true");
1339
+ try {
1340
+ const session = await this.client.verifyMagicLink(
1341
+ tokenInput.value.trim()
1342
+ );
1343
+ writeSession(this.apiKeyPrefix, {
1344
+ token: session.sessionToken,
1345
+ expiresAt: session.expiresAt,
1346
+ email: session.user.email,
1347
+ name: session.user.name
1348
+ });
1349
+ this.state.email = session.user.email;
1350
+ this.state.name = session.user.name;
1351
+ this.state.authStatus = "idle";
1352
+ this.go("list");
1353
+ } catch (err) {
1354
+ this.state.error = err.message;
1355
+ verifyBtn.removeAttribute("disabled");
1356
+ this.render();
1357
+ }
1358
+ });
1359
+ const back = document.createElement("button");
1360
+ back.className = "malto-link";
1361
+ back.style.marginTop = "12px";
1362
+ back.style.display = "block";
1363
+ back.textContent = "Use a different email";
1364
+ back.addEventListener("click", () => {
1365
+ this.state.authStatus = "idle";
1366
+ this.render();
1367
+ });
1368
+ wrap.appendChild(sent);
1369
+ wrap.appendChild(tokenInput);
1370
+ wrap.appendChild(verifyBtn);
1371
+ wrap.appendChild(back);
1372
+ return wrap;
1373
+ }
1374
+ const form = document.createElement("form");
1375
+ form.className = "malto-form";
1376
+ const intro = document.createElement("p");
1377
+ intro.style.fontSize = "13px";
1378
+ intro.style.color = "var(--malto-text-muted)";
1379
+ intro.style.marginBottom = "4px";
1380
+ intro.style.lineHeight = "1.5";
1381
+ intro.textContent = "Enter your email to receive a one-click sign-in link.";
1382
+ const email = document.createElement("input");
1383
+ email.className = "malto-input";
1384
+ email.type = "email";
1385
+ email.required = true;
1386
+ email.placeholder = "you@company.com";
1387
+ email.value = this.state.authEmail;
1388
+ const send = document.createElement("button");
1389
+ send.className = "malto-button";
1390
+ send.type = "submit";
1391
+ send.textContent = "Send sign-in link";
1392
+ form.appendChild(intro);
1393
+ form.appendChild(email);
1394
+ form.appendChild(send);
1395
+ form.addEventListener("submit", async (e) => {
1396
+ e.preventDefault();
1397
+ send.setAttribute("disabled", "true");
1398
+ try {
1399
+ await this.client.startMagicLink({ email: email.value.trim() });
1400
+ this.state.authEmail = email.value.trim();
1401
+ this.state.authStatus = "sent";
1402
+ this.render();
1403
+ } catch (err) {
1404
+ this.state.error = err.message;
1405
+ send.removeAttribute("disabled");
1406
+ this.render();
1407
+ }
1408
+ });
1409
+ return form;
1410
+ }
1411
+ viewRoadmap() {
1412
+ const wrap = document.createElement("div");
1413
+ if (this.state.loading) {
1414
+ wrap.appendChild(this.skeletonList(3));
1415
+ return wrap;
1416
+ }
1417
+ const labels = {
1418
+ PLANNED: "Planned",
1419
+ IN_PROGRESS: "In Progress",
1420
+ COMPLETED: "Done"
1421
+ };
1422
+ let any = false;
1423
+ for (const status of ["PLANNED", "IN_PROGRESS", "COMPLETED"]) {
1424
+ const items = this.state.roadmap[status] ?? [];
1425
+ const col = document.createElement("div");
1426
+ col.className = "malto-roadmap-col";
1427
+ const h = document.createElement("h5");
1428
+ h.textContent = labels[status] ?? status;
1429
+ const c = document.createElement("span");
1430
+ c.className = "malto-roadmap-count";
1431
+ c.textContent = String(items.length);
1432
+ h.appendChild(c);
1433
+ col.appendChild(h);
1434
+ if (items.length === 0) {
1435
+ const empty = document.createElement("div");
1436
+ empty.className = "malto-empty-desc";
1437
+ empty.style.padding = "8px 0 12px";
1438
+ empty.textContent = "Nothing here yet.";
1439
+ col.appendChild(empty);
1440
+ } else {
1441
+ any = true;
1442
+ for (const fb of items) col.appendChild(this.feedbackRow(fb));
1443
+ }
1444
+ wrap.appendChild(col);
1445
+ }
1446
+ if (!any && Object.keys(this.state.roadmap).length === 0) {
1447
+ wrap.innerHTML = "";
1448
+ wrap.appendChild(
1449
+ this.emptyState(
1450
+ "\u{1F5FA}\uFE0F",
1451
+ "Empty roadmap",
1452
+ "Once admins move items into Planned, In Progress or Done, you'll see them here."
1453
+ )
1454
+ );
1455
+ }
1456
+ return wrap;
1457
+ }
1458
+ viewChangelog() {
1459
+ const wrap = document.createElement("div");
1460
+ if (this.state.loading) {
1461
+ wrap.appendChild(this.skeletonList(2));
1462
+ return wrap;
1463
+ }
1464
+ if (this.state.releases.length === 0) {
1465
+ wrap.appendChild(
1466
+ this.emptyState("\u2728", "No updates yet", "Check back soon.")
1467
+ );
1468
+ return wrap;
1469
+ }
1470
+ for (const r of this.state.releases) {
1471
+ const item = document.createElement("div");
1472
+ item.className = "malto-release";
1473
+ if (r.publishedAt) {
1474
+ const d = document.createElement("span");
1475
+ d.className = "malto-release-date";
1476
+ d.textContent = formatDate(r.publishedAt);
1477
+ item.appendChild(d);
1478
+ }
1479
+ const t = document.createElement("h4");
1480
+ t.textContent = r.title;
1481
+ const b = document.createElement("p");
1482
+ b.textContent = r.body;
1483
+ item.appendChild(t);
1484
+ item.appendChild(b);
1485
+ wrap.appendChild(item);
1486
+ }
1487
+ return wrap;
1488
+ }
1489
+ viewDetail() {
1490
+ const wrap = document.createElement("div");
1491
+ const fb = this.state.selectedFeedback;
1492
+ if (!fb) {
1493
+ wrap.appendChild(
1494
+ this.emptyState("\u{1F4ED}", "Nothing selected", "Pick a request from Feed.")
1495
+ );
1496
+ return wrap;
1497
+ }
1498
+ const back = document.createElement("button");
1499
+ back.className = "malto-back";
1500
+ back.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg> Back';
1501
+ back.addEventListener("click", () => this.go("list"));
1502
+ wrap.appendChild(back);
1503
+ wrap.appendChild(this.feedbackRow(fb));
1504
+ const allowed = this.allowedViews();
1505
+ if (allowed.includes("comment")) {
1506
+ const commentsHeader = document.createElement("h5");
1507
+ commentsHeader.textContent = "Comments";
1508
+ commentsHeader.style.cssText = "margin: 16px 0 8px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--malto-text-muted); font-weight: 600;";
1509
+ wrap.appendChild(commentsHeader);
1510
+ if (this.state.loading) {
1511
+ wrap.appendChild(this.skeletonList(2));
1512
+ } else if (this.state.comments.length === 0) {
1513
+ const empty = document.createElement("div");
1514
+ empty.className = "malto-empty-desc";
1515
+ empty.style.padding = "8px 0 12px";
1516
+ empty.textContent = "No comments yet \u2014 start the conversation.";
1517
+ wrap.appendChild(empty);
1518
+ } else {
1519
+ for (const c of this.state.comments) {
1520
+ const node = document.createElement("div");
1521
+ node.className = "malto-comment";
1522
+ const av = document.createElement("div");
1523
+ av.className = "malto-avatar";
1524
+ const avatarUrl = safeImageUrl(c.author?.avatarUrl);
1525
+ if (avatarUrl) {
1526
+ const img = document.createElement("img");
1527
+ img.src = avatarUrl;
1528
+ img.alt = "";
1529
+ img.referrerPolicy = "no-referrer";
1530
+ img.crossOrigin = "anonymous";
1531
+ av.appendChild(img);
1532
+ } else {
1533
+ av.textContent = (c.author?.name ?? "?").charAt(0).toUpperCase();
1534
+ }
1535
+ const body = document.createElement("div");
1536
+ body.style.flex = "1";
1537
+ body.style.minWidth = "0";
1538
+ const top = document.createElement("div");
1539
+ const a = document.createElement("span");
1540
+ a.className = "malto-comment-author";
1541
+ a.textContent = c.author?.name ?? "User";
1542
+ const t = document.createElement("span");
1543
+ t.className = "malto-comment-time";
1544
+ t.textContent = relativeTime(c.createdAt);
1545
+ top.appendChild(a);
1546
+ top.appendChild(t);
1547
+ const txt = document.createElement("div");
1548
+ txt.className = "malto-comment-body";
1549
+ txt.textContent = c.body;
1550
+ body.appendChild(top);
1551
+ body.appendChild(txt);
1552
+ node.appendChild(av);
1553
+ node.appendChild(body);
1554
+ wrap.appendChild(node);
1555
+ }
1556
+ }
1557
+ const form = document.createElement("form");
1558
+ form.className = "malto-form";
1559
+ form.style.marginTop = "12px";
1560
+ const ta = document.createElement("textarea");
1561
+ ta.className = "malto-textarea";
1562
+ ta.placeholder = "Write a comment";
1563
+ ta.required = true;
1564
+ const send = document.createElement("button");
1565
+ send.className = "malto-button";
1566
+ send.type = "submit";
1567
+ send.textContent = "Post comment";
1568
+ form.appendChild(ta);
1569
+ form.appendChild(send);
1570
+ form.addEventListener("submit", async (e) => {
1571
+ e.preventDefault();
1572
+ if (!this.state.email) {
1573
+ this.go("auth");
1574
+ return;
1575
+ }
1576
+ send.setAttribute("disabled", "true");
1577
+ try {
1578
+ await this.client.addComment(fb.id, ta.value.trim());
1579
+ ta.value = "";
1580
+ await this.loadComments(fb.id);
1581
+ } catch (err) {
1582
+ this.state.error = err.message;
1583
+ } finally {
1584
+ send.removeAttribute("disabled");
1585
+ this.render();
1586
+ }
1587
+ });
1588
+ wrap.appendChild(form);
1589
+ }
1590
+ return wrap;
1591
+ }
1592
+ skeletonList(count) {
1593
+ const wrap = document.createElement("div");
1594
+ for (let i = 0; i < count; i++) {
1595
+ const row = document.createElement("div");
1596
+ row.className = "malto-skeleton-row";
1597
+ const v = document.createElement("div");
1598
+ v.className = "malto-skeleton";
1599
+ v.style.width = "44px";
1600
+ v.style.height = "48px";
1601
+ v.style.flex = "none";
1602
+ const body = document.createElement("div");
1603
+ body.style.flex = "1";
1604
+ const t = document.createElement("div");
1605
+ t.className = "malto-skeleton";
1606
+ t.style.height = "12px";
1607
+ t.style.width = "70%";
1608
+ t.style.marginBottom = "6px";
1609
+ const d = document.createElement("div");
1610
+ d.className = "malto-skeleton";
1611
+ d.style.height = "10px";
1612
+ d.style.width = "90%";
1613
+ body.appendChild(t);
1614
+ body.appendChild(d);
1615
+ row.appendChild(v);
1616
+ row.appendChild(body);
1617
+ wrap.appendChild(row);
1618
+ }
1619
+ return wrap;
1620
+ }
1621
+ emptyState(emoji, title, desc) {
1622
+ const wrap = document.createElement("div");
1623
+ wrap.className = "malto-empty";
1624
+ const icon = document.createElement("div");
1625
+ icon.className = "malto-empty-icon";
1626
+ icon.style.fontSize = "20px";
1627
+ icon.textContent = emoji;
1628
+ const t = document.createElement("div");
1629
+ t.className = "malto-empty-title";
1630
+ t.textContent = title;
1631
+ const d = document.createElement("div");
1632
+ d.className = "malto-empty-desc";
1633
+ d.textContent = desc;
1634
+ wrap.appendChild(icon);
1635
+ wrap.appendChild(t);
1636
+ wrap.appendChild(d);
1637
+ return wrap;
1638
+ }
1639
+ async loadFeedbacks() {
1640
+ this.state.loading = true;
1641
+ this.render();
1642
+ try {
1643
+ this.state.feedbacks = await this.client.listFeedbacks();
1644
+ } catch (err) {
1645
+ this.state.error = err.message;
1646
+ } finally {
1647
+ this.state.loading = false;
1648
+ this.render();
1649
+ }
1650
+ }
1651
+ async loadRoadmap() {
1652
+ this.state.loading = true;
1653
+ this.render();
1654
+ try {
1655
+ this.state.roadmap = await this.client.roadmap();
1656
+ } catch (err) {
1657
+ this.state.error = err.message;
1658
+ } finally {
1659
+ this.state.loading = false;
1660
+ this.render();
1661
+ }
1662
+ }
1663
+ async loadReleases() {
1664
+ this.state.loading = true;
1665
+ this.render();
1666
+ try {
1667
+ this.state.releases = await this.client.releases();
1668
+ } catch (err) {
1669
+ this.state.error = err.message;
1670
+ } finally {
1671
+ this.state.loading = false;
1672
+ this.render();
1673
+ }
1674
+ }
1675
+ async loadComments(id) {
1676
+ this.state.loading = true;
1677
+ this.render();
1678
+ try {
1679
+ this.state.comments = await this.client.comments(id);
1680
+ } catch (err) {
1681
+ this.state.error = err.message;
1682
+ } finally {
1683
+ this.state.loading = false;
1684
+ this.render();
1685
+ }
1686
+ }
1687
+ async handleVote(fb) {
1688
+ if (!this.state.email) {
1689
+ this.go("auth");
1690
+ return;
1691
+ }
1692
+ try {
1693
+ const result = await this.client.vote(fb.id);
1694
+ fb.hasVoted = result.voted;
1695
+ fb.votes = result.votes;
1696
+ this.render();
1697
+ } catch (err) {
1698
+ this.state.error = err.message;
1699
+ this.render();
1700
+ }
1701
+ }
1702
+ };
1703
+ function statusPill(status) {
1704
+ const pill = document.createElement("span");
1705
+ pill.className = `malto-status malto-status-${status}`;
1706
+ const dot = document.createElement("span");
1707
+ dot.className = "malto-status-dot";
1708
+ pill.appendChild(dot);
1709
+ const txt = document.createElement("span");
1710
+ const labels = {
1711
+ PLANNED: "Planned",
1712
+ IN_PROGRESS: "In progress",
1713
+ COMPLETED: "Done",
1714
+ DECLINED: "Declined",
1715
+ UNDER_REVIEW: "Under review",
1716
+ BACKLOG: "Backlog"
1717
+ };
1718
+ txt.textContent = labels[status] ?? status;
1719
+ pill.appendChild(txt);
1720
+ return pill;
1721
+ }
1722
+ function formatCount(n) {
1723
+ if (n < 1e3) return String(n);
1724
+ if (n < 1e4) return (n / 1e3).toFixed(1).replace(/\.0$/, "") + "k";
1725
+ return Math.floor(n / 1e3) + "k";
1726
+ }
1727
+ function relativeTime(iso) {
1728
+ const ms = Date.now() - new Date(iso).getTime();
1729
+ const s = Math.floor(ms / 1e3);
1730
+ if (s < 60) return "just now";
1731
+ const m = Math.floor(s / 60);
1732
+ if (m < 60) return `${m}m`;
1733
+ const h = Math.floor(m / 60);
1734
+ if (h < 24) return `${h}h`;
1735
+ const d = Math.floor(h / 24);
1736
+ if (d < 30) return `${d}d`;
1737
+ const mo = Math.floor(d / 30);
1738
+ if (mo < 12) return `${mo}mo`;
1739
+ return `${Math.floor(mo / 12)}y`;
1740
+ }
1741
+ function formatDate(iso) {
1742
+ try {
1743
+ return new Date(iso).toLocaleDateString(void 0, {
1744
+ day: "numeric",
1745
+ month: "short",
1746
+ year: "numeric"
1747
+ });
1748
+ } catch {
1749
+ return iso;
1750
+ }
1751
+ }
1752
+ function escape(s) {
1753
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1754
+ }
1755
+ function safeImageUrl(raw) {
1756
+ if (!raw || typeof raw !== "string") return null;
1757
+ let url;
1758
+ try {
1759
+ url = new URL(raw, typeof location !== "undefined" ? location.href : "https://x");
1760
+ } catch {
1761
+ return null;
1762
+ }
1763
+ if (url.protocol !== "https:" && url.protocol !== "http:") return null;
1764
+ return url.toString();
1765
+ }
1766
+
1767
+ // src/index.ts
1768
+ var activeWidget = null;
1769
+ function init(config) {
1770
+ if (activeWidget) {
1771
+ activeWidget.unmount();
1772
+ }
1773
+ const widget = new MaltoWidget(config);
1774
+ activeWidget = widget;
1775
+ void widget.mount();
1776
+ return widget;
1777
+ }
1778
+ function open() {
1779
+ activeWidget?.open();
1780
+ }
1781
+ function close() {
1782
+ activeWidget?.close();
1783
+ }
1784
+ function identify(payload) {
1785
+ activeWidget?.setIdentify(payload);
1786
+ }
1787
+ function reset(apiKey) {
1788
+ clearSession(apiKey.slice(0, 16));
1789
+ activeWidget?.setIdentify(null);
1790
+ }
1791
+ function destroy() {
1792
+ activeWidget?.unmount();
1793
+ activeWidget = null;
1794
+ }
1795
+
1796
+ exports.MaltoApiError = MaltoApiError;
1797
+ exports.MaltoClient = MaltoClient;
1798
+ exports.MaltoWidget = MaltoWidget;
1799
+ exports.close = close;
1800
+ exports.destroy = destroy;
1801
+ exports.identify = identify;
1802
+ exports.init = init;
1803
+ exports.open = open;
1804
+ exports.reset = reset;