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