@villetorio/lms-feedback-widget 1.0.2-dev

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,269 @@
1
+ # @villetorio/lms-svelte-feedback-widget
2
+
3
+ > Framework-agnostic floating feedback widget. Works in Sapper, SvelteKit, Next.js, React, Vue, and plain HTML — zero framework dependencies.
4
+
5
+ ---
6
+
7
+ ## Features
8
+
9
+ - ⭐ Star rating (1–5)
10
+ - 💬 Optional message textarea
11
+ - 🎨 LegalMatch brand styling (navy + gold)
12
+ - ✅ Success / error states
13
+ - ♿ Accessible (ARIA labels, keyboard support)
14
+ - 🌙 Click-outside to close
15
+ - 📦 Zero framework dependencies — pure TypeScript
16
+ - 🔌 Works in **any** JS project
17
+
18
+ ---
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ npm install @villetorio/lms-svelte-feedback-widget
24
+ ```
25
+
26
+ For projects with peer dependency conflicts (e.g. old Svelte 3 / Rollup 1):
27
+ ```bash
28
+ npm install @villetorio/lms-svelte-feedback-widget --legacy-peer-deps
29
+ ```
30
+
31
+ ---
32
+
33
+ ## Usage
34
+
35
+ > ⚠️ Always call `FeedbackWidget()` inside a **mount/effect hook** — never at the top level — because it needs `document.body` to exist first.
36
+
37
+ ---
38
+
39
+ ### Sapper
40
+
41
+ ```svelte
42
+ <!-- src/routes/_layout.svelte -->
43
+ <script>
44
+ import { onMount } from 'svelte';
45
+ import { FeedbackWidget } from '@villetorio/lms-svelte-feedback-widget';
46
+
47
+ onMount(() => {
48
+ FeedbackWidget({ apiRoute: '/api/feedback', appName: 'My App' });
49
+ });
50
+ </script>
51
+
52
+ <slot />
53
+ ```
54
+
55
+ API endpoint:
56
+ ```js
57
+ // src/routes/api/feedback.js
58
+ export async function post(req, res) {
59
+ const payload = req.body;
60
+ console.log('[Feedback]', payload);
61
+ res.writeHead(200, { 'Content-Type': 'application/json' });
62
+ res.end(JSON.stringify({ ok: true }));
63
+ }
64
+ ```
65
+
66
+ ---
67
+
68
+ ### SvelteKit
69
+
70
+ ```svelte
71
+ <!-- src/routes/+layout.svelte -->
72
+ <script>
73
+ import { onMount } from 'svelte';
74
+ import { FeedbackWidget } from '@villetorio/lms-svelte-feedback-widget';
75
+
76
+ onMount(() => {
77
+ FeedbackWidget({ apiRoute: '/api/feedback', appName: 'My App' });
78
+ });
79
+ </script>
80
+
81
+ <slot />
82
+ ```
83
+
84
+ API endpoint:
85
+ ```ts
86
+ // src/routes/api/feedback/+server.ts
87
+ import { json } from '@sveltejs/kit';
88
+ import type { RequestHandler } from './$types';
89
+
90
+ export const POST: RequestHandler = async ({ request }) => {
91
+ const payload = await request.json();
92
+ console.log('[Feedback]', payload);
93
+ return json({ ok: true });
94
+ };
95
+ ```
96
+
97
+ ---
98
+
99
+ ### Next.js (App Router)
100
+
101
+ ```tsx
102
+ // src/components/FeedbackWidgetLoader.tsx
103
+ 'use client';
104
+
105
+ import { useEffect } from 'react';
106
+ import { FeedbackWidget } from '@villetorio/lms-svelte-feedback-widget';
107
+
108
+ export default function FeedbackWidgetLoader() {
109
+ useEffect(() => {
110
+ FeedbackWidget({ apiRoute: '/api/feedback', appName: 'My App' });
111
+ }, []);
112
+
113
+ return null;
114
+ }
115
+ ```
116
+
117
+ ```tsx
118
+ // src/app/layout.tsx
119
+ import FeedbackWidgetLoader from '@/components/FeedbackWidgetLoader';
120
+
121
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
122
+ return (
123
+ <html lang="en">
124
+ <body>
125
+ {children}
126
+ <FeedbackWidgetLoader />
127
+ </body>
128
+ </html>
129
+ );
130
+ }
131
+ ```
132
+
133
+ API endpoint:
134
+ ```ts
135
+ // src/app/api/feedback/route.ts
136
+ import { NextRequest, NextResponse } from 'next/server';
137
+
138
+ export async function POST(req: NextRequest) {
139
+ const payload = await req.json();
140
+ console.log('[Feedback]', payload);
141
+ return NextResponse.json({ ok: true });
142
+ }
143
+ ```
144
+
145
+ ---
146
+
147
+ ### React
148
+
149
+ ```tsx
150
+ // FeedbackWidgetLoader.tsx
151
+ import { useEffect } from 'react';
152
+ import { FeedbackWidget } from '@villetorio/lms-svelte-feedback-widget';
153
+
154
+ export default function FeedbackWidgetLoader() {
155
+ useEffect(() => {
156
+ FeedbackWidget({ apiRoute: '/api/feedback', appName: 'My App' });
157
+ }, []);
158
+
159
+ return null;
160
+ }
161
+ ```
162
+
163
+ ```tsx
164
+ // App.tsx
165
+ import FeedbackWidgetLoader from './FeedbackWidgetLoader';
166
+
167
+ export default function App() {
168
+ return (
169
+ <>
170
+ <YourRoutes />
171
+ <FeedbackWidgetLoader />
172
+ </>
173
+ );
174
+ }
175
+ ```
176
+
177
+ ---
178
+
179
+ ### Vue
180
+
181
+ ```vue
182
+ <!-- App.vue or a layout component -->
183
+ <script setup>
184
+ import { onMounted } from 'vue';
185
+ import { FeedbackWidget } from '@villetorio/lms-svelte-feedback-widget';
186
+
187
+ onMounted(() => {
188
+ FeedbackWidget({ apiRoute: '/api/feedback', appName: 'My App' });
189
+ });
190
+ </script>
191
+
192
+ <template>
193
+ <RouterView />
194
+ </template>
195
+ ```
196
+
197
+ ---
198
+
199
+ ### Plain HTML (no bundler)
200
+
201
+ ```html
202
+ <script src="node_modules/@villetorio/lms-svelte-feedback-widget/dist/index.umd.js"></script>
203
+ <script>
204
+ LMSFeedbackWidget.FeedbackWidget({
205
+ apiRoute: '/api/feedback',
206
+ appName: 'My App'
207
+ });
208
+ </script>
209
+ ```
210
+
211
+ Or via CDN:
212
+ ```html
213
+ <script src="https://unpkg.com/@villetorio/lms-svelte-feedback-widget/dist/index.umd.js"></script>
214
+ <script>
215
+ LMSFeedbackWidget.FeedbackWidget({ apiRoute: '/api/feedback', appName: 'My App' });
216
+ </script>
217
+ ```
218
+
219
+ ---
220
+
221
+ ## Options
222
+
223
+ | Option | Type | Default | Description |
224
+ |---|---|---|---|
225
+ | `apiRoute` | `string` | `"/api/feedback"` | URL to POST feedback payload to |
226
+ | `apiKey` | `string` | `""` | Optional Bearer token for Authorization header |
227
+ | `title` | `string` | `"Share Your Feedback"` | Panel heading |
228
+ | `subtitle` | `string` | `"Help us improve"` | Panel subheading |
229
+ | `appName` | `string` | `"App"` | Included in payload and success message |
230
+ | `onSuccess` | `(payload: FeedbackPayload) => void` | `undefined` | Called after successful submission |
231
+ | `onError` | `(error: Error) => void` | `undefined` | Called when submission fails |
232
+
233
+ ---
234
+
235
+ ## Payload Shape
236
+
237
+ The widget POSTs this JSON to your `apiRoute`:
238
+
239
+ ```ts
240
+ interface FeedbackPayload {
241
+ rating: number; // 1–5 stars
242
+ message: string; // optional user text
243
+ app: string; // from appName option
244
+ timestamp: string; // ISO 8601 e.g. "2026-03-04T07:00:00.000Z"
245
+ url: string; // current page URL
246
+ }
247
+ ```
248
+
249
+ ---
250
+
251
+ ## Mount Hook Reference
252
+
253
+ | Framework | Hook |
254
+ |---|---|
255
+ | Svelte / Sapper | `onMount(() => { FeedbackWidget({...}) })` |
256
+ | SvelteKit | `onMount(() => { FeedbackWidget({...}) })` |
257
+ | React / Next.js | `useEffect(() => { FeedbackWidget({...}) }, [])` |
258
+ | Vue | `onMounted(() => { FeedbackWidget({...}) })` |
259
+ | Plain HTML | Bottom of `<body>` or `DOMContentLoaded` |
260
+
261
+ ---
262
+
263
+ ## Build Outputs
264
+
265
+ | File | Format | Use case |
266
+ |---|---|---|
267
+ | `dist/index.js` | ESM | SvelteKit, Vite, modern bundlers |
268
+ | `dist/index.cjs` | CJS | Node.js, Sapper, Rollup 1, Jest |
269
+ | `dist/index.umd.js` | UMD | Plain `<script>` tag, CDN |
@@ -0,0 +1,18 @@
1
+ export interface FeedbackPayload {
2
+ rating: number;
3
+ message: string;
4
+ app: string;
5
+ timestamp: string;
6
+ url: string;
7
+ }
8
+ interface FeedbackWidgetOptions {
9
+ apiRoute?: string;
10
+ apiKey?: string;
11
+ title?: string;
12
+ subtitle?: string;
13
+ appName?: string;
14
+ onSuccess?: (payload: FeedbackPayload) => void;
15
+ onError?: (error: Error) => void;
16
+ }
17
+ export declare function FeedbackWidget(opts?: FeedbackWidgetOptions): void;
18
+ export {};
package/dist/index.cjs ADDED
@@ -0,0 +1,239 @@
1
+ 'use strict';
2
+
3
+ const CSS = `
4
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');
5
+ .fw2-wrap { position:fixed; bottom:24px; right:24px; z-index:9999; font-family:'Inter',sans-serif; }
6
+ .fw2-fab {
7
+ display:flex; align-items:center; gap:8px;
8
+ background:#2c3e6b; color:#fff; border:none; cursor:pointer;
9
+ border-radius:24px; padding:12px 20px; font-size:14px; font-weight:500;
10
+ box-shadow:0 4px 16px rgba(0,0,0,0.25); transition:background .2s,transform .15s;
11
+ font-family:'Inter',sans-serif;
12
+ }
13
+ .fw2-fab:hover { background:#1e2e55; transform:translateY(-1px); }
14
+ .fw2-panel {
15
+ background:#fff; border-radius:12px; box-shadow:0 8px 40px rgba(0,0,0,0.18);
16
+ width:320px; overflow:hidden; margin-bottom:12px;
17
+ animation:fw2-pop .2s cubic-bezier(.34,1.4,.64,1) both;
18
+ }
19
+ @keyframes fw2-pop { from{opacity:0;transform:scale(.9) translateY(10px)} to{opacity:1;transform:scale(1) translateY(0)} }
20
+ .fw2-header {
21
+ background:#2c3e6b; color:#fff; padding:16px 18px 14px;
22
+ display:flex; align-items:flex-start; justify-content:space-between;
23
+ }
24
+ .fw2-header-text h3 { font-size:15px; font-weight:600; margin:0 0 3px; line-height:1.3; }
25
+ .fw2-header-text p { font-size:12px; opacity:.75; margin:0; }
26
+ .fw2-close-x { background:none; border:none; color:#fff; cursor:pointer; font-size:22px; line-height:1; opacity:.8; padding:0; margin-left:8px; }
27
+ .fw2-close-x:hover { opacity:1; }
28
+ .fw2-body { padding:20px 18px 18px; }
29
+ .fw2-question { font-size:13.5px; color:#333; text-align:center; margin-bottom:14px; }
30
+ .fw2-stars { display:flex; justify-content:center; gap:8px; margin-bottom:18px; }
31
+ .fw2-star {
32
+ background:none; border:none; padding:0; cursor:pointer; font-size:28px; line-height:1;
33
+ color:transparent; -webkit-text-stroke:1.5px #aaa; transition:-webkit-text-stroke .1s,color .1s,transform .1s;
34
+ }
35
+ .fw2-star:hover,.fw2-star.hover { -webkit-text-stroke:1.5px #c9a227; transform:scale(1.15); }
36
+ .fw2-star.active { -webkit-text-stroke:1.5px #c9a227; color:#c9a227; }
37
+ .fw2-textarea-label { font-size:13px; color:#444; margin-bottom:6px; display:block; }
38
+ .fw2-textarea-label span { color:#888; font-size:12px; }
39
+ .fw2-textarea {
40
+ width:100%; border:1px solid #d0d5dd; border-radius:6px; padding:10px 12px;
41
+ font-family:'Inter',sans-serif; font-size:13px; color:#333; resize:vertical; min-height:88px;
42
+ outline:none; transition:border-color .2s; background:#fff; margin-bottom:14px; box-sizing:border-box;
43
+ }
44
+ .fw2-textarea:focus { border-color:#2c3e6b; }
45
+ .fw2-submit {
46
+ width:100%; background:#6b7fa8; color:#fff; border:none; border-radius:6px; padding:11px;
47
+ font-family:'Inter',sans-serif; font-size:14px; font-weight:500; cursor:pointer;
48
+ display:flex; align-items:center; justify-content:center; gap:8px; transition:background .2s;
49
+ }
50
+ .fw2-submit:hover:not(:disabled) { background:#2c3e6b; }
51
+ .fw2-submit:disabled { opacity:.6; cursor:not-allowed; }
52
+ .fw2-error { font-size:12px; color:#dc2626; margin-bottom:10px; text-align:center; }
53
+ .fw2-success { padding:28px 18px 22px; text-align:center; }
54
+ .fw2-success-icon {
55
+ width:60px; height:60px; border-radius:50%; border:3px solid #c9a227;
56
+ display:flex; align-items:center; justify-content:center; margin:0 auto 16px; color:#c9a227;
57
+ }
58
+ .fw2-success h4 { font-size:18px; font-weight:600; color:#222; margin:0 0 8px; }
59
+ .fw2-success p { font-size:13px; color:#666; margin:0 0 20px; line-height:1.5; }
60
+ .fw2-submit-another {
61
+ background:#fff; border:1.5px solid #ccc; border-radius:6px; padding:9px 20px;
62
+ font-family:'Inter',sans-serif; font-size:13px; font-weight:500; color:#444; cursor:pointer;
63
+ }
64
+ .fw2-submit-another:hover { border-color:#2c3e6b; color:#2c3e6b; }
65
+ .fw2-close-pill { display:flex; align-items:center; justify-content:flex-end; margin-top:8px; }
66
+ .fw2-close-pill button {
67
+ background:#fff; border:1.5px solid #d0d5dd; border-radius:20px; padding:6px 16px;
68
+ font-family:'Inter',sans-serif; font-size:13px; color:#555; cursor:pointer;
69
+ display:flex; align-items:center; gap:6px; box-shadow:0 2px 8px rgba(0,0,0,0.08);
70
+ }
71
+ .fw2-close-pill button:hover { border-color:#999; }
72
+ `;
73
+ const ICON_SEND = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>`;
74
+ const ICON_CHAT = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="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>`;
75
+ const ICON_CHECK = `<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`;
76
+ const ICON_CLOSE = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`;
77
+ function FeedbackWidget(opts = {}) {
78
+ const { apiRoute = '/api/feedback', apiKey = '', title = 'Share Your Feedback', subtitle = 'Help us improve', appName = 'App', onSuccess, onError, } = opts;
79
+ // ── Inject styles once ──────────────────────────────────────────────────
80
+ if (!document.getElementById('fw2-styles')) {
81
+ const style = document.createElement('style');
82
+ style.id = 'fw2-styles';
83
+ style.textContent = CSS;
84
+ document.head.appendChild(style);
85
+ }
86
+ // ── State ───────────────────────────────────────────────────────────────
87
+ let open = false;
88
+ let rating = 0;
89
+ let hoverRating = 0;
90
+ let message = '';
91
+ let status = 'idle';
92
+ let errorMsg = '';
93
+ // ── Root element ────────────────────────────────────────────────────────
94
+ const wrap = document.createElement('div');
95
+ wrap.className = 'fw2-wrap';
96
+ document.body.appendChild(wrap);
97
+ // ── Render ───────────────────────────────────────────────────────────────
98
+ function render() {
99
+ wrap.innerHTML = open ? renderPanel() : renderFab();
100
+ bindEvents();
101
+ }
102
+ function renderFab() {
103
+ return `<button class="fw2-fab" id="fw2-fab-btn" aria-label="Open feedback">${ICON_CHAT} Feedback</button>`;
104
+ }
105
+ function renderPanel() {
106
+ return `
107
+ <div>
108
+ <div class="fw2-panel">
109
+ <div class="fw2-header">
110
+ <div class="fw2-header-text">
111
+ <h3>${title}</h3>
112
+ <p>${subtitle}</p>
113
+ </div>
114
+ <button class="fw2-close-x" id="fw2-close-x" aria-label="Close">×</button>
115
+ </div>
116
+ ${status === 'success' ? renderSuccess() : renderForm()}
117
+ </div>
118
+ <div class="fw2-close-pill">
119
+ <button id="fw2-close-pill">${ICON_CLOSE} Close</button>
120
+ </div>
121
+ </div>`;
122
+ }
123
+ function renderSuccess() {
124
+ return `
125
+ <div class="fw2-success">
126
+ <div class="fw2-success-icon">${ICON_CHECK}</div>
127
+ <h4>Thank You!</h4>
128
+ <p>Your feedback helps us improve the<br/>${appName} experience.</p>
129
+ <button class="fw2-submit-another" id="fw2-another">Submit Another</button>
130
+ </div>`;
131
+ }
132
+ function renderForm() {
133
+ const stars = [1, 2, 3, 4, 5].map(s => `
134
+ <button class="fw2-star${s <= rating ? ' active' : ''}${s <= hoverRating && s > rating ? ' hover' : ''}"
135
+ data-star="${s}" aria-label="${s} star${s > 1 ? 's' : ''}" aria-pressed="${s <= rating}">★</button>
136
+ `).join('');
137
+ return `
138
+ <div class="fw2-body">
139
+ <p class="fw2-question">How would you rate your experience?</p>
140
+ <div class="fw2-stars" role="radiogroup" aria-label="Star rating">${stars}</div>
141
+ <label class="fw2-textarea-label" for="fw2-message">Tell us more <span>(optional)</span></label>
142
+ <textarea id="fw2-message" class="fw2-textarea" placeholder="Share your thoughts...">${message}</textarea>
143
+ ${errorMsg ? `<div class="fw2-error" role="alert">${errorMsg}</div>` : ''}
144
+ <button class="fw2-submit" id="fw2-submit" ${status === 'loading' ? 'disabled' : ''}>
145
+ ${ICON_SEND} ${status === 'loading' ? 'Submitting…' : 'Submit Feedback'}
146
+ </button>
147
+ </div>`;
148
+ }
149
+ // ── Event binding ────────────────────────────────────────────────────────
150
+ function bindEvents() {
151
+ var _a, _b, _c, _d, _e;
152
+ // FAB
153
+ (_a = wrap.querySelector('#fw2-fab-btn')) === null || _a === void 0 ? void 0 : _a.addEventListener('click', () => { open = true; render(); });
154
+ // Close buttons
155
+ (_b = wrap.querySelector('#fw2-close-x')) === null || _b === void 0 ? void 0 : _b.addEventListener('click', () => { open = false; render(); });
156
+ (_c = wrap.querySelector('#fw2-close-pill')) === null || _c === void 0 ? void 0 : _c.addEventListener('click', () => { open = false; render(); });
157
+ // Submit another
158
+ (_d = wrap.querySelector('#fw2-another')) === null || _d === void 0 ? void 0 : _d.addEventListener('click', () => {
159
+ rating = 0;
160
+ hoverRating = 0;
161
+ message = '';
162
+ status = 'idle';
163
+ errorMsg = '';
164
+ render();
165
+ });
166
+ // Stars
167
+ wrap.querySelectorAll('.fw2-star').forEach(btn => {
168
+ btn.addEventListener('click', () => {
169
+ rating = Number(btn.dataset.star);
170
+ render();
171
+ });
172
+ btn.addEventListener('mouseenter', () => {
173
+ hoverRating = Number(btn.dataset.star);
174
+ render();
175
+ });
176
+ btn.addEventListener('mouseleave', () => {
177
+ hoverRating = 0;
178
+ render();
179
+ });
180
+ });
181
+ // Textarea — preserve value without full re-render
182
+ const ta = wrap.querySelector('#fw2-message');
183
+ ta === null || ta === void 0 ? void 0 : ta.addEventListener('input', () => { message = ta.value; });
184
+ // Submit
185
+ (_e = wrap.querySelector('#fw2-submit')) === null || _e === void 0 ? void 0 : _e.addEventListener('click', () => void handleSubmit());
186
+ // Click outside to close
187
+ document.addEventListener('mousedown', handleClickOutside);
188
+ }
189
+ function handleClickOutside(e) {
190
+ if (open && !wrap.contains(e.target)) {
191
+ open = false;
192
+ render();
193
+ }
194
+ }
195
+ // ── Submit ───────────────────────────────────────────────────────────────
196
+ async function handleSubmit() {
197
+ if (!rating) {
198
+ errorMsg = 'Please select a star rating before submitting.';
199
+ render();
200
+ return;
201
+ }
202
+ status = 'loading';
203
+ errorMsg = '';
204
+ render();
205
+ const payload = {
206
+ rating,
207
+ message,
208
+ app: appName,
209
+ timestamp: new Date().toISOString(),
210
+ url: typeof window !== 'undefined' ? window.location.href : '',
211
+ };
212
+ try {
213
+ const headers = { 'Content-Type': 'application/json' };
214
+ if (apiKey)
215
+ headers['Authorization'] = `Bearer ${apiKey}`;
216
+ const res = await fetch(apiRoute, {
217
+ method: 'POST',
218
+ headers,
219
+ body: JSON.stringify(payload),
220
+ });
221
+ if (!res.ok)
222
+ throw new Error(`HTTP ${res.status}`);
223
+ status = 'success';
224
+ onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess(payload);
225
+ }
226
+ catch (err) {
227
+ const error = err instanceof Error ? err : new Error('Unknown error');
228
+ status = 'error';
229
+ errorMsg = 'Something went wrong. Please try again.';
230
+ onError === null || onError === void 0 ? void 0 : onError(error);
231
+ }
232
+ render();
233
+ }
234
+ // ── Init ─────────────────────────────────────────────────────────────────
235
+ render();
236
+ }
237
+
238
+ exports.FeedbackWidget = FeedbackWidget;
239
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","sources":["../src/FeedbackWidget.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export { FeedbackWidget } from './FeedbackWidget';
2
+ export type { FeedbackPayload } from './FeedbackWidget';
package/dist/index.js ADDED
@@ -0,0 +1,237 @@
1
+ const CSS = `
2
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');
3
+ .fw2-wrap { position:fixed; bottom:24px; right:24px; z-index:9999; font-family:'Inter',sans-serif; }
4
+ .fw2-fab {
5
+ display:flex; align-items:center; gap:8px;
6
+ background:#2c3e6b; color:#fff; border:none; cursor:pointer;
7
+ border-radius:24px; padding:12px 20px; font-size:14px; font-weight:500;
8
+ box-shadow:0 4px 16px rgba(0,0,0,0.25); transition:background .2s,transform .15s;
9
+ font-family:'Inter',sans-serif;
10
+ }
11
+ .fw2-fab:hover { background:#1e2e55; transform:translateY(-1px); }
12
+ .fw2-panel {
13
+ background:#fff; border-radius:12px; box-shadow:0 8px 40px rgba(0,0,0,0.18);
14
+ width:320px; overflow:hidden; margin-bottom:12px;
15
+ animation:fw2-pop .2s cubic-bezier(.34,1.4,.64,1) both;
16
+ }
17
+ @keyframes fw2-pop { from{opacity:0;transform:scale(.9) translateY(10px)} to{opacity:1;transform:scale(1) translateY(0)} }
18
+ .fw2-header {
19
+ background:#2c3e6b; color:#fff; padding:16px 18px 14px;
20
+ display:flex; align-items:flex-start; justify-content:space-between;
21
+ }
22
+ .fw2-header-text h3 { font-size:15px; font-weight:600; margin:0 0 3px; line-height:1.3; }
23
+ .fw2-header-text p { font-size:12px; opacity:.75; margin:0; }
24
+ .fw2-close-x { background:none; border:none; color:#fff; cursor:pointer; font-size:22px; line-height:1; opacity:.8; padding:0; margin-left:8px; }
25
+ .fw2-close-x:hover { opacity:1; }
26
+ .fw2-body { padding:20px 18px 18px; }
27
+ .fw2-question { font-size:13.5px; color:#333; text-align:center; margin-bottom:14px; }
28
+ .fw2-stars { display:flex; justify-content:center; gap:8px; margin-bottom:18px; }
29
+ .fw2-star {
30
+ background:none; border:none; padding:0; cursor:pointer; font-size:28px; line-height:1;
31
+ color:transparent; -webkit-text-stroke:1.5px #aaa; transition:-webkit-text-stroke .1s,color .1s,transform .1s;
32
+ }
33
+ .fw2-star:hover,.fw2-star.hover { -webkit-text-stroke:1.5px #c9a227; transform:scale(1.15); }
34
+ .fw2-star.active { -webkit-text-stroke:1.5px #c9a227; color:#c9a227; }
35
+ .fw2-textarea-label { font-size:13px; color:#444; margin-bottom:6px; display:block; }
36
+ .fw2-textarea-label span { color:#888; font-size:12px; }
37
+ .fw2-textarea {
38
+ width:100%; border:1px solid #d0d5dd; border-radius:6px; padding:10px 12px;
39
+ font-family:'Inter',sans-serif; font-size:13px; color:#333; resize:vertical; min-height:88px;
40
+ outline:none; transition:border-color .2s; background:#fff; margin-bottom:14px; box-sizing:border-box;
41
+ }
42
+ .fw2-textarea:focus { border-color:#2c3e6b; }
43
+ .fw2-submit {
44
+ width:100%; background:#6b7fa8; color:#fff; border:none; border-radius:6px; padding:11px;
45
+ font-family:'Inter',sans-serif; font-size:14px; font-weight:500; cursor:pointer;
46
+ display:flex; align-items:center; justify-content:center; gap:8px; transition:background .2s;
47
+ }
48
+ .fw2-submit:hover:not(:disabled) { background:#2c3e6b; }
49
+ .fw2-submit:disabled { opacity:.6; cursor:not-allowed; }
50
+ .fw2-error { font-size:12px; color:#dc2626; margin-bottom:10px; text-align:center; }
51
+ .fw2-success { padding:28px 18px 22px; text-align:center; }
52
+ .fw2-success-icon {
53
+ width:60px; height:60px; border-radius:50%; border:3px solid #c9a227;
54
+ display:flex; align-items:center; justify-content:center; margin:0 auto 16px; color:#c9a227;
55
+ }
56
+ .fw2-success h4 { font-size:18px; font-weight:600; color:#222; margin:0 0 8px; }
57
+ .fw2-success p { font-size:13px; color:#666; margin:0 0 20px; line-height:1.5; }
58
+ .fw2-submit-another {
59
+ background:#fff; border:1.5px solid #ccc; border-radius:6px; padding:9px 20px;
60
+ font-family:'Inter',sans-serif; font-size:13px; font-weight:500; color:#444; cursor:pointer;
61
+ }
62
+ .fw2-submit-another:hover { border-color:#2c3e6b; color:#2c3e6b; }
63
+ .fw2-close-pill { display:flex; align-items:center; justify-content:flex-end; margin-top:8px; }
64
+ .fw2-close-pill button {
65
+ background:#fff; border:1.5px solid #d0d5dd; border-radius:20px; padding:6px 16px;
66
+ font-family:'Inter',sans-serif; font-size:13px; color:#555; cursor:pointer;
67
+ display:flex; align-items:center; gap:6px; box-shadow:0 2px 8px rgba(0,0,0,0.08);
68
+ }
69
+ .fw2-close-pill button:hover { border-color:#999; }
70
+ `;
71
+ const ICON_SEND = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>`;
72
+ const ICON_CHAT = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="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>`;
73
+ const ICON_CHECK = `<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`;
74
+ const ICON_CLOSE = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`;
75
+ function FeedbackWidget(opts = {}) {
76
+ const { apiRoute = '/api/feedback', apiKey = '', title = 'Share Your Feedback', subtitle = 'Help us improve', appName = 'App', onSuccess, onError, } = opts;
77
+ // ── Inject styles once ──────────────────────────────────────────────────
78
+ if (!document.getElementById('fw2-styles')) {
79
+ const style = document.createElement('style');
80
+ style.id = 'fw2-styles';
81
+ style.textContent = CSS;
82
+ document.head.appendChild(style);
83
+ }
84
+ // ── State ───────────────────────────────────────────────────────────────
85
+ let open = false;
86
+ let rating = 0;
87
+ let hoverRating = 0;
88
+ let message = '';
89
+ let status = 'idle';
90
+ let errorMsg = '';
91
+ // ── Root element ────────────────────────────────────────────────────────
92
+ const wrap = document.createElement('div');
93
+ wrap.className = 'fw2-wrap';
94
+ document.body.appendChild(wrap);
95
+ // ── Render ───────────────────────────────────────────────────────────────
96
+ function render() {
97
+ wrap.innerHTML = open ? renderPanel() : renderFab();
98
+ bindEvents();
99
+ }
100
+ function renderFab() {
101
+ return `<button class="fw2-fab" id="fw2-fab-btn" aria-label="Open feedback">${ICON_CHAT} Feedback</button>`;
102
+ }
103
+ function renderPanel() {
104
+ return `
105
+ <div>
106
+ <div class="fw2-panel">
107
+ <div class="fw2-header">
108
+ <div class="fw2-header-text">
109
+ <h3>${title}</h3>
110
+ <p>${subtitle}</p>
111
+ </div>
112
+ <button class="fw2-close-x" id="fw2-close-x" aria-label="Close">×</button>
113
+ </div>
114
+ ${status === 'success' ? renderSuccess() : renderForm()}
115
+ </div>
116
+ <div class="fw2-close-pill">
117
+ <button id="fw2-close-pill">${ICON_CLOSE} Close</button>
118
+ </div>
119
+ </div>`;
120
+ }
121
+ function renderSuccess() {
122
+ return `
123
+ <div class="fw2-success">
124
+ <div class="fw2-success-icon">${ICON_CHECK}</div>
125
+ <h4>Thank You!</h4>
126
+ <p>Your feedback helps us improve the<br/>${appName} experience.</p>
127
+ <button class="fw2-submit-another" id="fw2-another">Submit Another</button>
128
+ </div>`;
129
+ }
130
+ function renderForm() {
131
+ const stars = [1, 2, 3, 4, 5].map(s => `
132
+ <button class="fw2-star${s <= rating ? ' active' : ''}${s <= hoverRating && s > rating ? ' hover' : ''}"
133
+ data-star="${s}" aria-label="${s} star${s > 1 ? 's' : ''}" aria-pressed="${s <= rating}">★</button>
134
+ `).join('');
135
+ return `
136
+ <div class="fw2-body">
137
+ <p class="fw2-question">How would you rate your experience?</p>
138
+ <div class="fw2-stars" role="radiogroup" aria-label="Star rating">${stars}</div>
139
+ <label class="fw2-textarea-label" for="fw2-message">Tell us more <span>(optional)</span></label>
140
+ <textarea id="fw2-message" class="fw2-textarea" placeholder="Share your thoughts...">${message}</textarea>
141
+ ${errorMsg ? `<div class="fw2-error" role="alert">${errorMsg}</div>` : ''}
142
+ <button class="fw2-submit" id="fw2-submit" ${status === 'loading' ? 'disabled' : ''}>
143
+ ${ICON_SEND} ${status === 'loading' ? 'Submitting…' : 'Submit Feedback'}
144
+ </button>
145
+ </div>`;
146
+ }
147
+ // ── Event binding ────────────────────────────────────────────────────────
148
+ function bindEvents() {
149
+ var _a, _b, _c, _d, _e;
150
+ // FAB
151
+ (_a = wrap.querySelector('#fw2-fab-btn')) === null || _a === void 0 ? void 0 : _a.addEventListener('click', () => { open = true; render(); });
152
+ // Close buttons
153
+ (_b = wrap.querySelector('#fw2-close-x')) === null || _b === void 0 ? void 0 : _b.addEventListener('click', () => { open = false; render(); });
154
+ (_c = wrap.querySelector('#fw2-close-pill')) === null || _c === void 0 ? void 0 : _c.addEventListener('click', () => { open = false; render(); });
155
+ // Submit another
156
+ (_d = wrap.querySelector('#fw2-another')) === null || _d === void 0 ? void 0 : _d.addEventListener('click', () => {
157
+ rating = 0;
158
+ hoverRating = 0;
159
+ message = '';
160
+ status = 'idle';
161
+ errorMsg = '';
162
+ render();
163
+ });
164
+ // Stars
165
+ wrap.querySelectorAll('.fw2-star').forEach(btn => {
166
+ btn.addEventListener('click', () => {
167
+ rating = Number(btn.dataset.star);
168
+ render();
169
+ });
170
+ btn.addEventListener('mouseenter', () => {
171
+ hoverRating = Number(btn.dataset.star);
172
+ render();
173
+ });
174
+ btn.addEventListener('mouseleave', () => {
175
+ hoverRating = 0;
176
+ render();
177
+ });
178
+ });
179
+ // Textarea — preserve value without full re-render
180
+ const ta = wrap.querySelector('#fw2-message');
181
+ ta === null || ta === void 0 ? void 0 : ta.addEventListener('input', () => { message = ta.value; });
182
+ // Submit
183
+ (_e = wrap.querySelector('#fw2-submit')) === null || _e === void 0 ? void 0 : _e.addEventListener('click', () => void handleSubmit());
184
+ // Click outside to close
185
+ document.addEventListener('mousedown', handleClickOutside);
186
+ }
187
+ function handleClickOutside(e) {
188
+ if (open && !wrap.contains(e.target)) {
189
+ open = false;
190
+ render();
191
+ }
192
+ }
193
+ // ── Submit ───────────────────────────────────────────────────────────────
194
+ async function handleSubmit() {
195
+ if (!rating) {
196
+ errorMsg = 'Please select a star rating before submitting.';
197
+ render();
198
+ return;
199
+ }
200
+ status = 'loading';
201
+ errorMsg = '';
202
+ render();
203
+ const payload = {
204
+ rating,
205
+ message,
206
+ app: appName,
207
+ timestamp: new Date().toISOString(),
208
+ url: typeof window !== 'undefined' ? window.location.href : '',
209
+ };
210
+ try {
211
+ const headers = { 'Content-Type': 'application/json' };
212
+ if (apiKey)
213
+ headers['Authorization'] = `Bearer ${apiKey}`;
214
+ const res = await fetch(apiRoute, {
215
+ method: 'POST',
216
+ headers,
217
+ body: JSON.stringify(payload),
218
+ });
219
+ if (!res.ok)
220
+ throw new Error(`HTTP ${res.status}`);
221
+ status = 'success';
222
+ onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess(payload);
223
+ }
224
+ catch (err) {
225
+ const error = err instanceof Error ? err : new Error('Unknown error');
226
+ status = 'error';
227
+ errorMsg = 'Something went wrong. Please try again.';
228
+ onError === null || onError === void 0 ? void 0 : onError(error);
229
+ }
230
+ render();
231
+ }
232
+ // ── Init ─────────────────────────────────────────────────────────────────
233
+ render();
234
+ }
235
+
236
+ export { FeedbackWidget };
237
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/FeedbackWidget.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}
@@ -0,0 +1,245 @@
1
+ (function (global, factory) {
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.LMSFeedbackWidget = {}));
5
+ })(this, (function (exports) { 'use strict';
6
+
7
+ const CSS = `
8
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');
9
+ .fw2-wrap { position:fixed; bottom:24px; right:24px; z-index:9999; font-family:'Inter',sans-serif; }
10
+ .fw2-fab {
11
+ display:flex; align-items:center; gap:8px;
12
+ background:#2c3e6b; color:#fff; border:none; cursor:pointer;
13
+ border-radius:24px; padding:12px 20px; font-size:14px; font-weight:500;
14
+ box-shadow:0 4px 16px rgba(0,0,0,0.25); transition:background .2s,transform .15s;
15
+ font-family:'Inter',sans-serif;
16
+ }
17
+ .fw2-fab:hover { background:#1e2e55; transform:translateY(-1px); }
18
+ .fw2-panel {
19
+ background:#fff; border-radius:12px; box-shadow:0 8px 40px rgba(0,0,0,0.18);
20
+ width:320px; overflow:hidden; margin-bottom:12px;
21
+ animation:fw2-pop .2s cubic-bezier(.34,1.4,.64,1) both;
22
+ }
23
+ @keyframes fw2-pop { from{opacity:0;transform:scale(.9) translateY(10px)} to{opacity:1;transform:scale(1) translateY(0)} }
24
+ .fw2-header {
25
+ background:#2c3e6b; color:#fff; padding:16px 18px 14px;
26
+ display:flex; align-items:flex-start; justify-content:space-between;
27
+ }
28
+ .fw2-header-text h3 { font-size:15px; font-weight:600; margin:0 0 3px; line-height:1.3; }
29
+ .fw2-header-text p { font-size:12px; opacity:.75; margin:0; }
30
+ .fw2-close-x { background:none; border:none; color:#fff; cursor:pointer; font-size:22px; line-height:1; opacity:.8; padding:0; margin-left:8px; }
31
+ .fw2-close-x:hover { opacity:1; }
32
+ .fw2-body { padding:20px 18px 18px; }
33
+ .fw2-question { font-size:13.5px; color:#333; text-align:center; margin-bottom:14px; }
34
+ .fw2-stars { display:flex; justify-content:center; gap:8px; margin-bottom:18px; }
35
+ .fw2-star {
36
+ background:none; border:none; padding:0; cursor:pointer; font-size:28px; line-height:1;
37
+ color:transparent; -webkit-text-stroke:1.5px #aaa; transition:-webkit-text-stroke .1s,color .1s,transform .1s;
38
+ }
39
+ .fw2-star:hover,.fw2-star.hover { -webkit-text-stroke:1.5px #c9a227; transform:scale(1.15); }
40
+ .fw2-star.active { -webkit-text-stroke:1.5px #c9a227; color:#c9a227; }
41
+ .fw2-textarea-label { font-size:13px; color:#444; margin-bottom:6px; display:block; }
42
+ .fw2-textarea-label span { color:#888; font-size:12px; }
43
+ .fw2-textarea {
44
+ width:100%; border:1px solid #d0d5dd; border-radius:6px; padding:10px 12px;
45
+ font-family:'Inter',sans-serif; font-size:13px; color:#333; resize:vertical; min-height:88px;
46
+ outline:none; transition:border-color .2s; background:#fff; margin-bottom:14px; box-sizing:border-box;
47
+ }
48
+ .fw2-textarea:focus { border-color:#2c3e6b; }
49
+ .fw2-submit {
50
+ width:100%; background:#6b7fa8; color:#fff; border:none; border-radius:6px; padding:11px;
51
+ font-family:'Inter',sans-serif; font-size:14px; font-weight:500; cursor:pointer;
52
+ display:flex; align-items:center; justify-content:center; gap:8px; transition:background .2s;
53
+ }
54
+ .fw2-submit:hover:not(:disabled) { background:#2c3e6b; }
55
+ .fw2-submit:disabled { opacity:.6; cursor:not-allowed; }
56
+ .fw2-error { font-size:12px; color:#dc2626; margin-bottom:10px; text-align:center; }
57
+ .fw2-success { padding:28px 18px 22px; text-align:center; }
58
+ .fw2-success-icon {
59
+ width:60px; height:60px; border-radius:50%; border:3px solid #c9a227;
60
+ display:flex; align-items:center; justify-content:center; margin:0 auto 16px; color:#c9a227;
61
+ }
62
+ .fw2-success h4 { font-size:18px; font-weight:600; color:#222; margin:0 0 8px; }
63
+ .fw2-success p { font-size:13px; color:#666; margin:0 0 20px; line-height:1.5; }
64
+ .fw2-submit-another {
65
+ background:#fff; border:1.5px solid #ccc; border-radius:6px; padding:9px 20px;
66
+ font-family:'Inter',sans-serif; font-size:13px; font-weight:500; color:#444; cursor:pointer;
67
+ }
68
+ .fw2-submit-another:hover { border-color:#2c3e6b; color:#2c3e6b; }
69
+ .fw2-close-pill { display:flex; align-items:center; justify-content:flex-end; margin-top:8px; }
70
+ .fw2-close-pill button {
71
+ background:#fff; border:1.5px solid #d0d5dd; border-radius:20px; padding:6px 16px;
72
+ font-family:'Inter',sans-serif; font-size:13px; color:#555; cursor:pointer;
73
+ display:flex; align-items:center; gap:6px; box-shadow:0 2px 8px rgba(0,0,0,0.08);
74
+ }
75
+ .fw2-close-pill button:hover { border-color:#999; }
76
+ `;
77
+ const ICON_SEND = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>`;
78
+ const ICON_CHAT = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="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>`;
79
+ const ICON_CHECK = `<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`;
80
+ const ICON_CLOSE = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`;
81
+ function FeedbackWidget(opts = {}) {
82
+ const { apiRoute = '/api/feedback', apiKey = '', title = 'Share Your Feedback', subtitle = 'Help us improve', appName = 'App', onSuccess, onError, } = opts;
83
+ // ── Inject styles once ──────────────────────────────────────────────────
84
+ if (!document.getElementById('fw2-styles')) {
85
+ const style = document.createElement('style');
86
+ style.id = 'fw2-styles';
87
+ style.textContent = CSS;
88
+ document.head.appendChild(style);
89
+ }
90
+ // ── State ───────────────────────────────────────────────────────────────
91
+ let open = false;
92
+ let rating = 0;
93
+ let hoverRating = 0;
94
+ let message = '';
95
+ let status = 'idle';
96
+ let errorMsg = '';
97
+ // ── Root element ────────────────────────────────────────────────────────
98
+ const wrap = document.createElement('div');
99
+ wrap.className = 'fw2-wrap';
100
+ document.body.appendChild(wrap);
101
+ // ── Render ───────────────────────────────────────────────────────────────
102
+ function render() {
103
+ wrap.innerHTML = open ? renderPanel() : renderFab();
104
+ bindEvents();
105
+ }
106
+ function renderFab() {
107
+ return `<button class="fw2-fab" id="fw2-fab-btn" aria-label="Open feedback">${ICON_CHAT} Feedback</button>`;
108
+ }
109
+ function renderPanel() {
110
+ return `
111
+ <div>
112
+ <div class="fw2-panel">
113
+ <div class="fw2-header">
114
+ <div class="fw2-header-text">
115
+ <h3>${title}</h3>
116
+ <p>${subtitle}</p>
117
+ </div>
118
+ <button class="fw2-close-x" id="fw2-close-x" aria-label="Close">×</button>
119
+ </div>
120
+ ${status === 'success' ? renderSuccess() : renderForm()}
121
+ </div>
122
+ <div class="fw2-close-pill">
123
+ <button id="fw2-close-pill">${ICON_CLOSE} Close</button>
124
+ </div>
125
+ </div>`;
126
+ }
127
+ function renderSuccess() {
128
+ return `
129
+ <div class="fw2-success">
130
+ <div class="fw2-success-icon">${ICON_CHECK}</div>
131
+ <h4>Thank You!</h4>
132
+ <p>Your feedback helps us improve the<br/>${appName} experience.</p>
133
+ <button class="fw2-submit-another" id="fw2-another">Submit Another</button>
134
+ </div>`;
135
+ }
136
+ function renderForm() {
137
+ const stars = [1, 2, 3, 4, 5].map(s => `
138
+ <button class="fw2-star${s <= rating ? ' active' : ''}${s <= hoverRating && s > rating ? ' hover' : ''}"
139
+ data-star="${s}" aria-label="${s} star${s > 1 ? 's' : ''}" aria-pressed="${s <= rating}">★</button>
140
+ `).join('');
141
+ return `
142
+ <div class="fw2-body">
143
+ <p class="fw2-question">How would you rate your experience?</p>
144
+ <div class="fw2-stars" role="radiogroup" aria-label="Star rating">${stars}</div>
145
+ <label class="fw2-textarea-label" for="fw2-message">Tell us more <span>(optional)</span></label>
146
+ <textarea id="fw2-message" class="fw2-textarea" placeholder="Share your thoughts...">${message}</textarea>
147
+ ${errorMsg ? `<div class="fw2-error" role="alert">${errorMsg}</div>` : ''}
148
+ <button class="fw2-submit" id="fw2-submit" ${status === 'loading' ? 'disabled' : ''}>
149
+ ${ICON_SEND} ${status === 'loading' ? 'Submitting…' : 'Submit Feedback'}
150
+ </button>
151
+ </div>`;
152
+ }
153
+ // ── Event binding ────────────────────────────────────────────────────────
154
+ function bindEvents() {
155
+ var _a, _b, _c, _d, _e;
156
+ // FAB
157
+ (_a = wrap.querySelector('#fw2-fab-btn')) === null || _a === void 0 ? void 0 : _a.addEventListener('click', () => { open = true; render(); });
158
+ // Close buttons
159
+ (_b = wrap.querySelector('#fw2-close-x')) === null || _b === void 0 ? void 0 : _b.addEventListener('click', () => { open = false; render(); });
160
+ (_c = wrap.querySelector('#fw2-close-pill')) === null || _c === void 0 ? void 0 : _c.addEventListener('click', () => { open = false; render(); });
161
+ // Submit another
162
+ (_d = wrap.querySelector('#fw2-another')) === null || _d === void 0 ? void 0 : _d.addEventListener('click', () => {
163
+ rating = 0;
164
+ hoverRating = 0;
165
+ message = '';
166
+ status = 'idle';
167
+ errorMsg = '';
168
+ render();
169
+ });
170
+ // Stars
171
+ wrap.querySelectorAll('.fw2-star').forEach(btn => {
172
+ btn.addEventListener('click', () => {
173
+ rating = Number(btn.dataset.star);
174
+ render();
175
+ });
176
+ btn.addEventListener('mouseenter', () => {
177
+ hoverRating = Number(btn.dataset.star);
178
+ render();
179
+ });
180
+ btn.addEventListener('mouseleave', () => {
181
+ hoverRating = 0;
182
+ render();
183
+ });
184
+ });
185
+ // Textarea — preserve value without full re-render
186
+ const ta = wrap.querySelector('#fw2-message');
187
+ ta === null || ta === void 0 ? void 0 : ta.addEventListener('input', () => { message = ta.value; });
188
+ // Submit
189
+ (_e = wrap.querySelector('#fw2-submit')) === null || _e === void 0 ? void 0 : _e.addEventListener('click', () => void handleSubmit());
190
+ // Click outside to close
191
+ document.addEventListener('mousedown', handleClickOutside);
192
+ }
193
+ function handleClickOutside(e) {
194
+ if (open && !wrap.contains(e.target)) {
195
+ open = false;
196
+ render();
197
+ }
198
+ }
199
+ // ── Submit ───────────────────────────────────────────────────────────────
200
+ async function handleSubmit() {
201
+ if (!rating) {
202
+ errorMsg = 'Please select a star rating before submitting.';
203
+ render();
204
+ return;
205
+ }
206
+ status = 'loading';
207
+ errorMsg = '';
208
+ render();
209
+ const payload = {
210
+ rating,
211
+ message,
212
+ app: appName,
213
+ timestamp: new Date().toISOString(),
214
+ url: typeof window !== 'undefined' ? window.location.href : '',
215
+ };
216
+ try {
217
+ const headers = { 'Content-Type': 'application/json' };
218
+ if (apiKey)
219
+ headers['Authorization'] = `Bearer ${apiKey}`;
220
+ const res = await fetch(apiRoute, {
221
+ method: 'POST',
222
+ headers,
223
+ body: JSON.stringify(payload),
224
+ });
225
+ if (!res.ok)
226
+ throw new Error(`HTTP ${res.status}`);
227
+ status = 'success';
228
+ onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess(payload);
229
+ }
230
+ catch (err) {
231
+ const error = err instanceof Error ? err : new Error('Unknown error');
232
+ status = 'error';
233
+ errorMsg = 'Something went wrong. Please try again.';
234
+ onError === null || onError === void 0 ? void 0 : onError(error);
235
+ }
236
+ render();
237
+ }
238
+ // ── Init ─────────────────────────────────────────────────────────────────
239
+ render();
240
+ }
241
+
242
+ exports.FeedbackWidget = FeedbackWidget;
243
+
244
+ }));
245
+ //# sourceMappingURL=index.umd.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.umd.js","sources":["../src/FeedbackWidget.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@villetorio/lms-feedback-widget",
3
+ "version": "1.0.2-dev",
4
+ "description": "Floating feedback widget — framework agnostic, works in Svelte, SvelteKit, Sapper, React, Vue, plain HTML",
5
+ "license": "UNLICENSED",
6
+ "private": false,
7
+ "publishConfig": {
8
+ "registry": "https://registry.npmjs.org/",
9
+ "access": "public"
10
+ },
11
+ "main": "./dist/index.cjs",
12
+ "module": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js",
18
+ "require": "./dist/index.cjs",
19
+ "default": "./dist/index.umd.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "scripts": {
26
+ "build": "rollup -c rollup.config.js",
27
+ "dev": "rollup -c rollup.config.js --watch",
28
+ "type-check": "tsc --noEmit",
29
+ "prepublishOnly": "npm run build"
30
+ },
31
+ "devDependencies": {
32
+ "@rollup/plugin-node-resolve": "^15.0.0",
33
+ "@rollup/plugin-typescript": "^11.0.0",
34
+ "rollup": "^4.0.0",
35
+ "tslib": "^2.6.0",
36
+ "typescript": "^5.0.0"
37
+ },
38
+ "keywords": [
39
+ "svelte",
40
+ "react",
41
+ "sapper",
42
+ "feedback",
43
+ "widget",
44
+ "vanilla"
45
+ ]
46
+ }