@patch-adams/plugin-feedback 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1020 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var zod = require('zod');
6
+
7
+ // src/config.ts
8
+ var IssueSubtypeSchema = zod.z.object({
9
+ /** Unique identifier for the subtype */
10
+ id: zod.z.string().min(1),
11
+ /** Display label for the subtype */
12
+ label: zod.z.string().min(1)
13
+ });
14
+ var IssueTypeSchema = zod.z.object({
15
+ /** Unique identifier for the issue type */
16
+ id: zod.z.string().min(1),
17
+ /** Display label for the issue type */
18
+ label: zod.z.string().min(1),
19
+ /** Optional subtypes that appear when this type is selected */
20
+ subtypes: zod.z.array(IssueSubtypeSchema).optional()
21
+ });
22
+ var MetadataOptionsSchema = zod.z.object({
23
+ /** Include the course ID from LRS bridge (if available) */
24
+ courseId: zod.z.boolean().default(true),
25
+ /** Include the current lesson/page ID (URL hash or pathname) */
26
+ lessonId: zod.z.boolean().default(true),
27
+ /** Include the browser user agent */
28
+ userAgent: zod.z.boolean().default(true),
29
+ /** Include timestamp of submission */
30
+ timestamp: zod.z.boolean().default(true),
31
+ /** Include the full current URL */
32
+ url: zod.z.boolean().default(true)
33
+ });
34
+ var DEFAULT_ISSUE_TYPES = [
35
+ {
36
+ id: "content",
37
+ label: "Content Issue",
38
+ subtypes: [
39
+ { id: "typo", label: "Typo / Grammar" },
40
+ { id: "incorrect", label: "Incorrect Information" },
41
+ { id: "unclear", label: "Unclear / Confusing" },
42
+ { id: "outdated", label: "Outdated Content" }
43
+ ]
44
+ },
45
+ {
46
+ id: "technical",
47
+ label: "Technical Issue",
48
+ subtypes: [
49
+ { id: "audio", label: "Audio Problem" },
50
+ { id: "video", label: "Video Problem" },
51
+ { id: "navigation", label: "Navigation Issue" },
52
+ { id: "display", label: "Display / Layout Issue" }
53
+ ]
54
+ },
55
+ {
56
+ id: "suggestion",
57
+ label: "Suggestion"
58
+ },
59
+ {
60
+ id: "other",
61
+ label: "Other"
62
+ }
63
+ ];
64
+ var FeedbackConfigSchema = zod.z.object({
65
+ /** Whether the feedback plugin is enabled */
66
+ enabled: zod.z.boolean().default(true),
67
+ // === Endpoint Configuration ===
68
+ /** API endpoint URL for submitting feedback (optional - if not set, logs to console) */
69
+ endpoint: zod.z.string().url().optional(),
70
+ /** HTTP method for the endpoint (default: POST) */
71
+ method: zod.z.enum(["POST", "PUT"]).default("POST"),
72
+ /** Additional headers to send with the request */
73
+ headers: zod.z.record(zod.z.string()).optional(),
74
+ // === Appearance ===
75
+ /** Position of the feedback tab (left or right side of screen) */
76
+ position: zod.z.enum(["left", "right"]).default("right"),
77
+ /** Text displayed on the feedback tab */
78
+ tabText: zod.z.string().default("Feedback"),
79
+ /** Background color of the feedback tab */
80
+ tabColor: zod.z.string().default("#007bff"),
81
+ /** Text color of the feedback tab */
82
+ tabTextColor: zod.z.string().default("#ffffff"),
83
+ /** Z-index for the feedback widget (default: 9999) */
84
+ zIndex: zod.z.number().default(9999),
85
+ // === Form Fields ===
86
+ /** Show star rating field */
87
+ showRating: zod.z.boolean().default(true),
88
+ /** Whether rating is required to submit */
89
+ ratingRequired: zod.z.boolean().default(false),
90
+ /** Number of stars in the rating (default: 5) */
91
+ ratingStars: zod.z.number().min(3).max(10).default(5),
92
+ /** Show issue type dropdown */
93
+ showIssueType: zod.z.boolean().default(true),
94
+ /** Whether issue type is required to submit */
95
+ issueTypeRequired: zod.z.boolean().default(false),
96
+ /** Available issue types (uses defaults if not provided) */
97
+ issueTypes: zod.z.array(IssueTypeSchema).optional(),
98
+ /** Show comment textarea */
99
+ showComment: zod.z.boolean().default(true),
100
+ /** Whether comment is required to submit */
101
+ commentRequired: zod.z.boolean().default(false),
102
+ /** Maximum length for comments (default: 500) */
103
+ commentMaxLength: zod.z.number().min(50).max(5e3).default(500),
104
+ /** Placeholder text for comment field (uses translation if not set) */
105
+ commentPlaceholder: zod.z.string().optional(),
106
+ // === Language / i18n ===
107
+ /** Locale for UI text (en or fr) */
108
+ locale: zod.z.enum(["en", "fr"]).default("en"),
109
+ /** Custom translations (overrides built-in translations) */
110
+ translations: zod.z.record(zod.z.string()).optional(),
111
+ // === Metadata ===
112
+ /** What metadata to include with feedback submissions */
113
+ includeMetadata: MetadataOptionsSchema.default({}),
114
+ // === Behavior ===
115
+ /** Auto-close modal after successful submission (default: true) */
116
+ autoClose: zod.z.boolean().default(true),
117
+ /** Delay in ms before auto-closing after success (default: 2000) */
118
+ autoCloseDelay: zod.z.number().min(500).max(1e4).default(2e3),
119
+ /** Enable debug logging (default: false) */
120
+ debug: zod.z.boolean().default(false)
121
+ });
122
+
123
+ // src/i18n/en.json
124
+ var en_default = {
125
+ title: "Send Feedback",
126
+ ratingLabel: "How would you rate this content?",
127
+ issueTypeLabel: "What type of feedback?",
128
+ selectIssueType: "Select type...",
129
+ subtypeLabel: "More specifically:",
130
+ selectSubtype: "Select...",
131
+ commentLabel: "Your feedback",
132
+ commentPlaceholder: "Please describe your feedback in detail...",
133
+ submit: "Send Feedback",
134
+ submitting: "Sending...",
135
+ thankYou: "Thank you for your feedback!",
136
+ errorSubmitting: "Failed to send feedback. Please try again.",
137
+ errorRequired: "Please fill in the required fields.",
138
+ characterCount: "{current}/{max}"
139
+ };
140
+
141
+ // src/i18n/fr.json
142
+ var fr_default = {
143
+ title: "Envoyer un commentaire",
144
+ ratingLabel: "Comment \xE9valuez-vous ce contenu?",
145
+ issueTypeLabel: "Type de commentaire",
146
+ selectIssueType: "S\xE9lectionner...",
147
+ subtypeLabel: "Plus pr\xE9cis\xE9ment:",
148
+ selectSubtype: "S\xE9lectionner...",
149
+ commentLabel: "Votre commentaire",
150
+ commentPlaceholder: "Veuillez d\xE9crire votre commentaire en d\xE9tail...",
151
+ submit: "Envoyer",
152
+ submitting: "Envoi en cours...",
153
+ thankYou: "Merci pour votre commentaire!",
154
+ errorSubmitting: "\xC9chec de l'envoi. Veuillez r\xE9essayer.",
155
+ errorRequired: "Veuillez remplir les champs obligatoires.",
156
+ characterCount: "{current}/{max}"
157
+ };
158
+
159
+ // src/i18n/index.ts
160
+ var translations = {
161
+ en: en_default,
162
+ fr: fr_default
163
+ };
164
+ function getTranslations(locale, overrides) {
165
+ const base = translations[locale] || translations.en;
166
+ if (overrides) {
167
+ return { ...base, ...overrides };
168
+ }
169
+ return base;
170
+ }
171
+
172
+ // src/widget.ts
173
+ function generateFeedbackWidget(config) {
174
+ const t = getTranslations(config.locale, config.translations);
175
+ const issueTypes = config.issueTypes || DEFAULT_ISSUE_TYPES;
176
+ return `
177
+ (function() {
178
+ 'use strict';
179
+
180
+ // ============================================================================
181
+ // FEEDBACK WIDGET - Patch-Adams Plugin v1.0.0
182
+ // ============================================================================
183
+
184
+ var FEEDBACK = window.pa_patcher = window.pa_patcher || {};
185
+ FEEDBACK.feedback = {
186
+ version: '1.0.0',
187
+ isOpen: false,
188
+ rating: 0,
189
+ config: ${JSON.stringify({
190
+ endpoint: config.endpoint,
191
+ method: config.method,
192
+ headers: config.headers,
193
+ position: config.position,
194
+ showRating: config.showRating,
195
+ ratingRequired: config.ratingRequired,
196
+ ratingStars: config.ratingStars,
197
+ showIssueType: config.showIssueType,
198
+ issueTypeRequired: config.issueTypeRequired,
199
+ showComment: config.showComment,
200
+ commentRequired: config.commentRequired,
201
+ commentMaxLength: config.commentMaxLength,
202
+ autoClose: config.autoClose,
203
+ autoCloseDelay: config.autoCloseDelay,
204
+ debug: config.debug,
205
+ includeMetadata: config.includeMetadata
206
+ })},
207
+ translations: ${JSON.stringify(t)},
208
+ issueTypes: ${JSON.stringify(issueTypes)},
209
+ };
210
+
211
+ var FB = FEEDBACK.feedback;
212
+
213
+ function log() {
214
+ if (FB.config.debug) {
215
+ console.log.apply(console, ['[PA-Feedback]'].concat(Array.prototype.slice.call(arguments)));
216
+ }
217
+ }
218
+
219
+ // ============================================================================
220
+ // DOM CREATION
221
+ // ============================================================================
222
+
223
+ function createWidget() {
224
+ var container = document.createElement('div');
225
+ container.id = 'pa-feedback-container';
226
+ container.className = 'pa-feedback-${config.position}';
227
+ container.innerHTML = buildWidgetHtml();
228
+ document.body.appendChild(container);
229
+ setupEventListeners();
230
+ log('Widget created');
231
+ }
232
+
233
+ function buildWidgetHtml() {
234
+ var html = '';
235
+
236
+ // Tab button
237
+ html += '<button id="pa-feedback-tab" class="pa-feedback-tab" aria-label="' + FB.translations.title + '">';
238
+ html += '<span class="pa-feedback-tab-text">${config.tabText}</span>';
239
+ html += '</button>';
240
+
241
+ // Modal
242
+ html += '<div id="pa-feedback-modal" class="pa-feedback-modal pa-feedback-hidden" role="dialog" aria-modal="true" aria-labelledby="pa-feedback-title">';
243
+ html += '<div class="pa-feedback-content">';
244
+
245
+ // Header
246
+ html += '<div class="pa-feedback-header">';
247
+ html += '<h3 id="pa-feedback-title">' + FB.translations.title + '</h3>';
248
+ html += '<button id="pa-feedback-close" class="pa-feedback-close" aria-label="Close">&times;</button>';
249
+ html += '</div>';
250
+
251
+ // Form
252
+ html += '<form id="pa-feedback-form">';
253
+
254
+ // Rating (if enabled)
255
+ ${config.showRating ? `
256
+ html += '<div class="pa-feedback-field">';
257
+ html += '<label class="pa-feedback-label">' + FB.translations.ratingLabel + '${config.ratingRequired ? ' <span class="pa-feedback-required">*</span>' : ""}</label>';
258
+ html += '<div class="pa-feedback-stars" role="radiogroup" aria-label="Rating">';
259
+ for (var i = 1; i <= ${config.ratingStars}; i++) {
260
+ html += '<button type="button" class="pa-feedback-star" data-value="' + i + '" role="radio" aria-checked="false" aria-label="' + i + ' star">&#9734;</button>';
261
+ }
262
+ html += '</div>';
263
+ html += '</div>';
264
+ ` : ""}
265
+
266
+ // Issue Type (if enabled)
267
+ ${config.showIssueType ? `
268
+ html += '<div class="pa-feedback-field">';
269
+ html += '<label class="pa-feedback-label" for="pa-feedback-issue-type">' + FB.translations.issueTypeLabel + '${config.issueTypeRequired ? ' <span class="pa-feedback-required">*</span>' : ""}</label>';
270
+ html += '<select id="pa-feedback-issue-type" class="pa-feedback-select">';
271
+ html += '<option value="">' + FB.translations.selectIssueType + '</option>';
272
+ FB.issueTypes.forEach(function(type) {
273
+ html += '<option value="' + type.id + '">' + type.label + '</option>';
274
+ });
275
+ html += '</select>';
276
+ html += '</div>';
277
+
278
+ // Subtype container (hidden by default)
279
+ html += '<div id="pa-feedback-subtype-field" class="pa-feedback-field pa-feedback-hidden">';
280
+ html += '<label class="pa-feedback-label" for="pa-feedback-subtype">' + FB.translations.subtypeLabel + '</label>';
281
+ html += '<select id="pa-feedback-subtype" class="pa-feedback-select">';
282
+ html += '<option value="">' + FB.translations.selectSubtype + '</option>';
283
+ html += '</select>';
284
+ html += '</div>';
285
+ ` : ""}
286
+
287
+ // Comment (if enabled)
288
+ ${config.showComment ? `
289
+ html += '<div class="pa-feedback-field">';
290
+ html += '<label class="pa-feedback-label" for="pa-feedback-comment">' + FB.translations.commentLabel + '${config.commentRequired ? ' <span class="pa-feedback-required">*</span>' : ""}</label>';
291
+ html += '<textarea id="pa-feedback-comment" class="pa-feedback-textarea" maxlength="${config.commentMaxLength}" placeholder="${config.commentPlaceholder || ""}" rows="4"></textarea>';
292
+ html += '<div class="pa-feedback-counter"><span id="pa-feedback-char-count">0</span>/${config.commentMaxLength}</div>';
293
+ html += '</div>';
294
+ ` : ""}
295
+
296
+ // Error message area
297
+ html += '<div id="pa-feedback-error" class="pa-feedback-error pa-feedback-hidden"></div>';
298
+
299
+ // Submit button
300
+ html += '<button type="submit" id="pa-feedback-submit" class="pa-feedback-submit">';
301
+ html += '<span class="pa-feedback-submit-text">' + FB.translations.submit + '</span>';
302
+ html += '<span class="pa-feedback-submit-loading pa-feedback-hidden">' + FB.translations.submitting + '</span>';
303
+ html += '</button>';
304
+
305
+ html += '</form>';
306
+
307
+ // Success message
308
+ html += '<div id="pa-feedback-success" class="pa-feedback-success pa-feedback-hidden">';
309
+ html += '<div class="pa-feedback-checkmark">&#10003;</div>';
310
+ html += '<p>' + FB.translations.thankYou + '</p>';
311
+ html += '</div>';
312
+
313
+ html += '</div>'; // content
314
+ html += '</div>'; // modal
315
+
316
+ return html;
317
+ }
318
+
319
+ // ============================================================================
320
+ // EVENT HANDLERS
321
+ // ============================================================================
322
+
323
+ function setupEventListeners() {
324
+ // Tab click
325
+ document.getElementById('pa-feedback-tab').addEventListener('click', toggleModal);
326
+
327
+ // Close button
328
+ document.getElementById('pa-feedback-close').addEventListener('click', closeModal);
329
+
330
+ // Form submit
331
+ document.getElementById('pa-feedback-form').addEventListener('submit', handleSubmit);
332
+
333
+ // Click outside to close
334
+ document.getElementById('pa-feedback-modal').addEventListener('click', function(e) {
335
+ if (e.target === this) closeModal();
336
+ });
337
+
338
+ // Escape key to close
339
+ document.addEventListener('keydown', function(e) {
340
+ if (e.key === 'Escape' && FB.isOpen) closeModal();
341
+ });
342
+
343
+ ${config.showRating ? `
344
+ // Star rating
345
+ var stars = document.querySelectorAll('.pa-feedback-star');
346
+ stars.forEach(function(star, index) {
347
+ star.addEventListener('click', function() {
348
+ FB.rating = index + 1;
349
+ updateStars();
350
+ });
351
+ star.addEventListener('mouseenter', function() {
352
+ highlightStars(index + 1);
353
+ });
354
+ });
355
+ document.querySelector('.pa-feedback-stars').addEventListener('mouseleave', function() {
356
+ highlightStars(FB.rating);
357
+ });
358
+ ` : ""}
359
+
360
+ ${config.showIssueType ? `
361
+ // Issue type change
362
+ document.getElementById('pa-feedback-issue-type').addEventListener('change', updateSubtypes);
363
+ ` : ""}
364
+
365
+ ${config.showComment ? `
366
+ // Character counter
367
+ document.getElementById('pa-feedback-comment').addEventListener('input', updateCharCount);
368
+ ` : ""}
369
+ }
370
+
371
+ function toggleModal() {
372
+ if (FB.isOpen) {
373
+ closeModal();
374
+ } else {
375
+ openModal();
376
+ }
377
+ }
378
+
379
+ function openModal() {
380
+ FB.isOpen = true;
381
+ document.getElementById('pa-feedback-modal').classList.remove('pa-feedback-hidden');
382
+ document.getElementById('pa-feedback-tab').classList.add('pa-feedback-tab-active');
383
+ log('Modal opened');
384
+ }
385
+
386
+ function closeModal() {
387
+ FB.isOpen = false;
388
+ document.getElementById('pa-feedback-modal').classList.add('pa-feedback-hidden');
389
+ document.getElementById('pa-feedback-tab').classList.remove('pa-feedback-tab-active');
390
+ log('Modal closed');
391
+ }
392
+
393
+ ${config.showRating ? `
394
+ function updateStars() {
395
+ var stars = document.querySelectorAll('.pa-feedback-star');
396
+ stars.forEach(function(star, index) {
397
+ var isFilled = index < FB.rating;
398
+ star.innerHTML = isFilled ? '&#9733;' : '&#9734;';
399
+ star.classList.toggle('pa-feedback-star-filled', isFilled);
400
+ star.setAttribute('aria-checked', isFilled ? 'true' : 'false');
401
+ });
402
+ }
403
+
404
+ function highlightStars(count) {
405
+ var stars = document.querySelectorAll('.pa-feedback-star');
406
+ stars.forEach(function(star, index) {
407
+ var isFilled = index < count;
408
+ star.innerHTML = isFilled ? '&#9733;' : '&#9734;';
409
+ star.classList.toggle('pa-feedback-star-hover', isFilled && count > FB.rating);
410
+ });
411
+ }
412
+ ` : ""}
413
+
414
+ ${config.showIssueType ? `
415
+ function updateSubtypes() {
416
+ var typeSelect = document.getElementById('pa-feedback-issue-type');
417
+ var subtypeField = document.getElementById('pa-feedback-subtype-field');
418
+ var subtypeSelect = document.getElementById('pa-feedback-subtype');
419
+ var selectedType = typeSelect.value;
420
+
421
+ // Find the selected issue type
422
+ var issueType = FB.issueTypes.find(function(t) { return t.id === selectedType; });
423
+
424
+ if (issueType && issueType.subtypes && issueType.subtypes.length > 0) {
425
+ // Populate subtypes
426
+ subtypeSelect.innerHTML = '<option value="">' + FB.translations.selectSubtype + '</option>';
427
+ issueType.subtypes.forEach(function(subtype) {
428
+ subtypeSelect.innerHTML += '<option value="' + subtype.id + '">' + subtype.label + '</option>';
429
+ });
430
+ subtypeField.classList.remove('pa-feedback-hidden');
431
+ } else {
432
+ subtypeField.classList.add('pa-feedback-hidden');
433
+ subtypeSelect.value = '';
434
+ }
435
+ }
436
+ ` : ""}
437
+
438
+ ${config.showComment ? `
439
+ function updateCharCount() {
440
+ var textarea = document.getElementById('pa-feedback-comment');
441
+ var counter = document.getElementById('pa-feedback-char-count');
442
+ counter.textContent = textarea.value.length;
443
+ }
444
+ ` : ""}
445
+
446
+ // ============================================================================
447
+ // FORM SUBMISSION
448
+ // ============================================================================
449
+
450
+ function validateForm() {
451
+ var errors = [];
452
+
453
+ ${config.showRating && config.ratingRequired ? `
454
+ if (FB.rating === 0) {
455
+ errors.push('Rating is required');
456
+ }
457
+ ` : ""}
458
+
459
+ ${config.showIssueType && config.issueTypeRequired ? `
460
+ if (!document.getElementById('pa-feedback-issue-type').value) {
461
+ errors.push('Issue type is required');
462
+ }
463
+ ` : ""}
464
+
465
+ ${config.showComment && config.commentRequired ? `
466
+ if (!document.getElementById('pa-feedback-comment').value.trim()) {
467
+ errors.push('Comment is required');
468
+ }
469
+ ` : ""}
470
+
471
+ return errors;
472
+ }
473
+
474
+ function collectFormData() {
475
+ var data = {};
476
+
477
+ ${config.showRating ? `data.rating = FB.rating;` : ""}
478
+ ${config.showIssueType ? `
479
+ data.issueType = document.getElementById('pa-feedback-issue-type').value || null;
480
+ data.issueSubtype = document.getElementById('pa-feedback-subtype').value || null;
481
+ ` : ""}
482
+ ${config.showComment ? `data.comment = document.getElementById('pa-feedback-comment').value.trim();` : ""}
483
+
484
+ // Add metadata
485
+ data.metadata = collectMetadata();
486
+
487
+ return data;
488
+ }
489
+
490
+ function collectMetadata() {
491
+ var meta = {};
492
+ var cfg = FB.config.includeMetadata;
493
+
494
+ if (cfg.courseId && window.pa_patcher && window.pa_patcher.lrs && window.pa_patcher.lrs.courseInfo) {
495
+ meta.courseId = window.pa_patcher.lrs.courseInfo.id;
496
+ meta.courseTitle = window.pa_patcher.lrs.courseInfo.title;
497
+ }
498
+
499
+ if (cfg.lessonId) {
500
+ meta.lessonId = window.location.hash || window.location.pathname;
501
+ }
502
+
503
+ if (cfg.userAgent) {
504
+ meta.userAgent = navigator.userAgent;
505
+ }
506
+
507
+ if (cfg.timestamp) {
508
+ meta.timestamp = new Date().toISOString();
509
+ }
510
+
511
+ if (cfg.url) {
512
+ meta.url = window.location.href;
513
+ }
514
+
515
+ return meta;
516
+ }
517
+
518
+ function showError(message) {
519
+ var errorEl = document.getElementById('pa-feedback-error');
520
+ errorEl.textContent = message;
521
+ errorEl.classList.remove('pa-feedback-hidden');
522
+ }
523
+
524
+ function hideError() {
525
+ document.getElementById('pa-feedback-error').classList.add('pa-feedback-hidden');
526
+ }
527
+
528
+ function setSubmitting(isSubmitting) {
529
+ var submitBtn = document.getElementById('pa-feedback-submit');
530
+ var textEl = submitBtn.querySelector('.pa-feedback-submit-text');
531
+ var loadingEl = submitBtn.querySelector('.pa-feedback-submit-loading');
532
+
533
+ submitBtn.disabled = isSubmitting;
534
+ textEl.classList.toggle('pa-feedback-hidden', isSubmitting);
535
+ loadingEl.classList.toggle('pa-feedback-hidden', !isSubmitting);
536
+ }
537
+
538
+ function showSuccess() {
539
+ document.getElementById('pa-feedback-form').classList.add('pa-feedback-hidden');
540
+ document.getElementById('pa-feedback-success').classList.remove('pa-feedback-hidden');
541
+
542
+ if (FB.config.autoClose) {
543
+ setTimeout(function() {
544
+ closeModal();
545
+ resetForm();
546
+ }, FB.config.autoCloseDelay);
547
+ }
548
+ }
549
+
550
+ function resetForm() {
551
+ var form = document.getElementById('pa-feedback-form');
552
+ form.reset();
553
+ form.classList.remove('pa-feedback-hidden');
554
+ document.getElementById('pa-feedback-success').classList.add('pa-feedback-hidden');
555
+ hideError();
556
+
557
+ FB.rating = 0;
558
+ ${config.showRating ? "updateStars();" : ""}
559
+ ${config.showComment ? "updateCharCount();" : ""}
560
+ ${config.showIssueType ? `
561
+ document.getElementById('pa-feedback-subtype-field').classList.add('pa-feedback-hidden');
562
+ ` : ""}
563
+ }
564
+
565
+ async function handleSubmit(e) {
566
+ e.preventDefault();
567
+ hideError();
568
+
569
+ // Validate
570
+ var errors = validateForm();
571
+ if (errors.length > 0) {
572
+ showError(FB.translations.errorRequired);
573
+ return;
574
+ }
575
+
576
+ // Collect data
577
+ var data = collectFormData();
578
+ log('Submitting:', data);
579
+
580
+ // Submit
581
+ setSubmitting(true);
582
+
583
+ try {
584
+ ${config.endpoint ? `
585
+ var response = await fetch('${config.endpoint}', {
586
+ method: '${config.method}',
587
+ headers: Object.assign({
588
+ 'Content-Type': 'application/json',
589
+ }, FB.config.headers || {}),
590
+ body: JSON.stringify(data),
591
+ });
592
+
593
+ if (!response.ok) {
594
+ throw new Error('HTTP ' + response.status);
595
+ }
596
+
597
+ log('Submitted successfully');
598
+ ` : `
599
+ // No endpoint configured - just log
600
+ console.log('[PA-Feedback] Feedback submitted:', data);
601
+ log('No endpoint configured, feedback logged to console');
602
+ `}
603
+
604
+ showSuccess();
605
+ } catch (err) {
606
+ log('Submit error:', err);
607
+ showError(FB.translations.errorSubmitting);
608
+ } finally {
609
+ setSubmitting(false);
610
+ }
611
+ }
612
+
613
+ // ============================================================================
614
+ // INITIALIZATION
615
+ // ============================================================================
616
+
617
+ function init() {
618
+ if (document.readyState === 'loading') {
619
+ document.addEventListener('DOMContentLoaded', createWidget);
620
+ } else {
621
+ createWidget();
622
+ }
623
+ log('Initialized');
624
+ }
625
+
626
+ init();
627
+
628
+ })();
629
+ `;
630
+ }
631
+
632
+ // src/styles.ts
633
+ function generateFeedbackStyles(config) {
634
+ const position = config.position;
635
+ const tabColor = config.tabColor;
636
+ const tabTextColor = config.tabTextColor;
637
+ const zIndex = config.zIndex;
638
+ return `
639
+ /* ============================================================================
640
+ FEEDBACK WIDGET STYLES - Patch-Adams Plugin v1.0.0
641
+ ============================================================================ */
642
+
643
+ #pa-feedback-container {
644
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
645
+ font-size: 14px;
646
+ line-height: 1.5;
647
+ color: #333;
648
+ box-sizing: border-box;
649
+ }
650
+
651
+ #pa-feedback-container *,
652
+ #pa-feedback-container *::before,
653
+ #pa-feedback-container *::after {
654
+ box-sizing: inherit;
655
+ }
656
+
657
+ /* Hidden utility class */
658
+ #pa-feedback-container .pa-feedback-hidden {
659
+ display: none !important;
660
+ }
661
+
662
+ /* ============================================================================
663
+ TAB BUTTON
664
+ ============================================================================ */
665
+
666
+ #pa-feedback-tab {
667
+ position: fixed;
668
+ ${position}: 0;
669
+ top: 50%;
670
+ transform: translateY(-50%) rotate(${position === "right" ? "-90deg" : "90deg"});
671
+ transform-origin: ${position === "right" ? "right center" : "left center"};
672
+ ${position === "right" ? "right: -32px;" : "left: -32px;"}
673
+ z-index: ${zIndex};
674
+ background: ${tabColor};
675
+ color: ${tabTextColor};
676
+ border: none;
677
+ padding: 10px 20px;
678
+ font-size: 14px;
679
+ font-weight: 600;
680
+ cursor: pointer;
681
+ border-radius: ${position === "right" ? "0 0 8px 8px" : "8px 8px 0 0"};
682
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
683
+ transition: all 0.2s ease;
684
+ white-space: nowrap;
685
+ }
686
+
687
+ #pa-feedback-tab:hover {
688
+ background: ${adjustColor(tabColor, -15)};
689
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
690
+ }
691
+
692
+ #pa-feedback-tab:focus {
693
+ outline: 2px solid ${tabColor};
694
+ outline-offset: 2px;
695
+ }
696
+
697
+ #pa-feedback-tab.pa-feedback-tab-active {
698
+ background: ${adjustColor(tabColor, -20)};
699
+ }
700
+
701
+ /* ============================================================================
702
+ MODAL
703
+ ============================================================================ */
704
+
705
+ #pa-feedback-modal {
706
+ position: fixed;
707
+ ${position}: 0;
708
+ top: 0;
709
+ width: 360px;
710
+ height: 100%;
711
+ z-index: ${zIndex + 1};
712
+ background: rgba(0, 0, 0, 0.3);
713
+ display: flex;
714
+ justify-content: flex-${position === "right" ? "end" : "start"};
715
+ animation: pa-feedback-fade-in 0.2s ease;
716
+ }
717
+
718
+ @keyframes pa-feedback-fade-in {
719
+ from { opacity: 0; }
720
+ to { opacity: 1; }
721
+ }
722
+
723
+ #pa-feedback-container .pa-feedback-content {
724
+ width: 100%;
725
+ max-width: 360px;
726
+ height: 100%;
727
+ background: #fff;
728
+ box-shadow: ${position === "right" ? "-4px" : "4px"} 0 20px rgba(0, 0, 0, 0.15);
729
+ display: flex;
730
+ flex-direction: column;
731
+ animation: pa-feedback-slide-in 0.2s ease;
732
+ }
733
+
734
+ @keyframes pa-feedback-slide-in {
735
+ from {
736
+ transform: translateX(${position === "right" ? "100%" : "-100%"});
737
+ }
738
+ to {
739
+ transform: translateX(0);
740
+ }
741
+ }
742
+
743
+ /* ============================================================================
744
+ HEADER
745
+ ============================================================================ */
746
+
747
+ #pa-feedback-container .pa-feedback-header {
748
+ display: flex;
749
+ justify-content: space-between;
750
+ align-items: center;
751
+ padding: 16px 20px;
752
+ border-bottom: 1px solid #e5e5e5;
753
+ background: #fafafa;
754
+ }
755
+
756
+ #pa-feedback-container .pa-feedback-header h3 {
757
+ margin: 0;
758
+ font-size: 18px;
759
+ font-weight: 600;
760
+ color: #222;
761
+ }
762
+
763
+ #pa-feedback-container .pa-feedback-close {
764
+ background: none;
765
+ border: none;
766
+ font-size: 24px;
767
+ color: #666;
768
+ cursor: pointer;
769
+ padding: 4px 8px;
770
+ line-height: 1;
771
+ border-radius: 4px;
772
+ transition: all 0.15s ease;
773
+ }
774
+
775
+ #pa-feedback-container .pa-feedback-close:hover {
776
+ background: #e5e5e5;
777
+ color: #333;
778
+ }
779
+
780
+ /* ============================================================================
781
+ FORM
782
+ ============================================================================ */
783
+
784
+ #pa-feedback-form {
785
+ flex: 1;
786
+ overflow-y: auto;
787
+ padding: 20px;
788
+ display: flex;
789
+ flex-direction: column;
790
+ gap: 20px;
791
+ }
792
+
793
+ #pa-feedback-container .pa-feedback-field {
794
+ display: flex;
795
+ flex-direction: column;
796
+ gap: 8px;
797
+ }
798
+
799
+ #pa-feedback-container .pa-feedback-label {
800
+ font-weight: 500;
801
+ color: #444;
802
+ font-size: 14px;
803
+ }
804
+
805
+ #pa-feedback-container .pa-feedback-required {
806
+ color: #dc3545;
807
+ }
808
+
809
+ /* ============================================================================
810
+ STAR RATING
811
+ ============================================================================ */
812
+
813
+ #pa-feedback-container .pa-feedback-stars {
814
+ display: flex;
815
+ gap: 4px;
816
+ }
817
+
818
+ #pa-feedback-container .pa-feedback-star {
819
+ background: none;
820
+ border: none;
821
+ font-size: 28px;
822
+ color: #ccc;
823
+ cursor: pointer;
824
+ padding: 4px;
825
+ line-height: 1;
826
+ transition: all 0.15s ease;
827
+ }
828
+
829
+ #pa-feedback-container .pa-feedback-star:hover {
830
+ transform: scale(1.1);
831
+ }
832
+
833
+ #pa-feedback-container .pa-feedback-star-filled,
834
+ #pa-feedback-container .pa-feedback-star-hover {
835
+ color: #ffc107;
836
+ }
837
+
838
+ /* ============================================================================
839
+ SELECT & TEXTAREA
840
+ ============================================================================ */
841
+
842
+ #pa-feedback-container .pa-feedback-select {
843
+ width: 100%;
844
+ padding: 10px 12px;
845
+ font-size: 14px;
846
+ border: 1px solid #ddd;
847
+ border-radius: 6px;
848
+ background: #fff;
849
+ color: #333;
850
+ cursor: pointer;
851
+ transition: border-color 0.15s ease;
852
+ }
853
+
854
+ #pa-feedback-container .pa-feedback-select:focus {
855
+ outline: none;
856
+ border-color: ${tabColor};
857
+ box-shadow: 0 0 0 3px ${hexToRgba(tabColor, 0.1)};
858
+ }
859
+
860
+ #pa-feedback-container .pa-feedback-textarea {
861
+ width: 100%;
862
+ padding: 12px;
863
+ font-size: 14px;
864
+ font-family: inherit;
865
+ border: 1px solid #ddd;
866
+ border-radius: 6px;
867
+ resize: vertical;
868
+ min-height: 100px;
869
+ transition: border-color 0.15s ease;
870
+ }
871
+
872
+ #pa-feedback-container .pa-feedback-textarea:focus {
873
+ outline: none;
874
+ border-color: ${tabColor};
875
+ box-shadow: 0 0 0 3px ${hexToRgba(tabColor, 0.1)};
876
+ }
877
+
878
+ #pa-feedback-container .pa-feedback-textarea::placeholder {
879
+ color: #999;
880
+ }
881
+
882
+ #pa-feedback-container .pa-feedback-counter {
883
+ text-align: right;
884
+ font-size: 12px;
885
+ color: #888;
886
+ }
887
+
888
+ /* ============================================================================
889
+ ERROR MESSAGE
890
+ ============================================================================ */
891
+
892
+ #pa-feedback-container .pa-feedback-error {
893
+ background: #fee2e2;
894
+ color: #dc2626;
895
+ padding: 10px 14px;
896
+ border-radius: 6px;
897
+ font-size: 13px;
898
+ }
899
+
900
+ /* ============================================================================
901
+ SUBMIT BUTTON
902
+ ============================================================================ */
903
+
904
+ #pa-feedback-container .pa-feedback-submit {
905
+ width: 100%;
906
+ padding: 12px 20px;
907
+ font-size: 16px;
908
+ font-weight: 600;
909
+ color: #fff;
910
+ background: ${tabColor};
911
+ border: none;
912
+ border-radius: 6px;
913
+ cursor: pointer;
914
+ transition: all 0.15s ease;
915
+ margin-top: auto;
916
+ }
917
+
918
+ #pa-feedback-container .pa-feedback-submit:hover:not(:disabled) {
919
+ background: ${adjustColor(tabColor, -15)};
920
+ }
921
+
922
+ #pa-feedback-container .pa-feedback-submit:focus {
923
+ outline: 2px solid ${tabColor};
924
+ outline-offset: 2px;
925
+ }
926
+
927
+ #pa-feedback-container .pa-feedback-submit:disabled {
928
+ opacity: 0.7;
929
+ cursor: not-allowed;
930
+ }
931
+
932
+ /* ============================================================================
933
+ SUCCESS MESSAGE
934
+ ============================================================================ */
935
+
936
+ #pa-feedback-container .pa-feedback-success {
937
+ flex: 1;
938
+ display: flex;
939
+ flex-direction: column;
940
+ align-items: center;
941
+ justify-content: center;
942
+ padding: 40px 20px;
943
+ text-align: center;
944
+ }
945
+
946
+ #pa-feedback-container .pa-feedback-checkmark {
947
+ width: 64px;
948
+ height: 64px;
949
+ background: #10b981;
950
+ color: #fff;
951
+ border-radius: 50%;
952
+ display: flex;
953
+ align-items: center;
954
+ justify-content: center;
955
+ font-size: 32px;
956
+ margin-bottom: 20px;
957
+ animation: pa-feedback-pop 0.3s ease;
958
+ }
959
+
960
+ @keyframes pa-feedback-pop {
961
+ 0% { transform: scale(0); }
962
+ 50% { transform: scale(1.2); }
963
+ 100% { transform: scale(1); }
964
+ }
965
+
966
+ #pa-feedback-container .pa-feedback-success p {
967
+ margin: 0;
968
+ font-size: 18px;
969
+ font-weight: 500;
970
+ color: #333;
971
+ }
972
+ `;
973
+ }
974
+ function adjustColor(hex, percent) {
975
+ hex = hex.replace(/^#/, "");
976
+ let r = parseInt(hex.substring(0, 2), 16);
977
+ let g = parseInt(hex.substring(2, 4), 16);
978
+ let b = parseInt(hex.substring(4, 6), 16);
979
+ r = Math.min(255, Math.max(0, r + r * percent / 100));
980
+ g = Math.min(255, Math.max(0, g + g * percent / 100));
981
+ b = Math.min(255, Math.max(0, b + b * percent / 100));
982
+ return "#" + Math.round(r).toString(16).padStart(2, "0") + Math.round(g).toString(16).padStart(2, "0") + Math.round(b).toString(16).padStart(2, "0");
983
+ }
984
+ function hexToRgba(hex, alpha) {
985
+ hex = hex.replace(/^#/, "");
986
+ const r = parseInt(hex.substring(0, 2), 16);
987
+ const g = parseInt(hex.substring(2, 4), 16);
988
+ const b = parseInt(hex.substring(4, 6), 16);
989
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
990
+ }
991
+
992
+ // src/index.ts
993
+ var feedbackPlugin = {
994
+ name: "feedback",
995
+ version: "1.0.0",
996
+ description: "Inject a feedback form widget into Rise courses for collecting user feedback",
997
+ configSchema: FeedbackConfigSchema,
998
+ generateCss(config) {
999
+ return {
1000
+ after: generateFeedbackStyles(config)
1001
+ };
1002
+ },
1003
+ generateJs(config) {
1004
+ return {
1005
+ after: generateFeedbackWidget(config)
1006
+ };
1007
+ }
1008
+ };
1009
+ var index_default = feedbackPlugin;
1010
+
1011
+ exports.DEFAULT_ISSUE_TYPES = DEFAULT_ISSUE_TYPES;
1012
+ exports.FeedbackConfigSchema = FeedbackConfigSchema;
1013
+ exports.default = index_default;
1014
+ exports.feedbackPlugin = feedbackPlugin;
1015
+ exports.generateFeedbackStyles = generateFeedbackStyles;
1016
+ exports.generateFeedbackWidget = generateFeedbackWidget;
1017
+ exports.getTranslations = getTranslations;
1018
+ exports.translations = translations;
1019
+ //# sourceMappingURL=index.cjs.map
1020
+ //# sourceMappingURL=index.cjs.map