@e-llm-studio/feedback-flow 0.0.0-stage → 0.1.32

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.
@@ -0,0 +1,659 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FeedbackModal = FeedbackModal;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const react_1 = require("react");
6
+ const react_dom_1 = require("react-dom");
7
+ const lucide_react_1 = require("lucide-react");
8
+ const types_1 = require("./types");
9
+ const feedbackTheme_1 = require("./feedbackTheme");
10
+ const FeedbackScreenshotThumbnail_1 = require("./FeedbackScreenshotThumbnail");
11
+ const severities = ['critical', 'high', 'medium', 'low'];
12
+ const wordCount = (value) => value.trim().split(/\s+/).filter(Boolean).length;
13
+ function FeedbackModal(props) {
14
+ if (!props.flow)
15
+ return null;
16
+ return (0, jsx_runtime_1.jsx)(FeedbackModalContent, { ...props });
17
+ }
18
+ function FeedbackModalContent({ flow, copy: copyOverrides, theme, onSubmit, onProgress, onDismiss, saving = false, resolveScreenshotUrl, anchorRef, overlayHostRef, anchorKey, slots, className, style, styles, classNames, backdropClassName, backdropStyle, dialogClassName, dialogStyle, }) {
19
+ const copy = (0, types_1.createFeedbackCopy)(copyOverrides);
20
+ const mergedTheme = (0, types_1.createFeedbackTheme)(theme);
21
+ const [customTime, setCustomTime] = (0, react_1.useState)(false);
22
+ const [introDismissed, setIntroDismissed] = (0, react_1.useState)(false);
23
+ const [showUseful, setShowUseful] = (0, react_1.useState)(false);
24
+ const [showLowRatingTime, setShowLowRatingTime] = (0, react_1.useState)(false);
25
+ const [revealedGapCount, setRevealedGapCount] = (0, react_1.useState)(0);
26
+ const [activeGapIndex, setActiveGapIndex] = (0, react_1.useState)(0);
27
+ const [reviewPageIndex, setReviewPageIndex] = (0, react_1.useState)(0);
28
+ const [submitError, setSubmitError] = (0, react_1.useState)(false);
29
+ const [isSubmitting, setIsSubmitting] = (0, react_1.useState)(false);
30
+ const [screenshotPreview, setScreenshotPreview] = (0, react_1.useState)(null);
31
+ const [capturingScreenshot, setCapturingScreenshot] = (0, react_1.useState)(false);
32
+ const [captureError, setCaptureError] = (0, react_1.useState)('');
33
+ const submittingRef = (0, react_1.useRef)(false);
34
+ const [position, setPosition] = (0, react_1.useState)({ left: 12, top: 12, width: 480, maxHeight: 600 });
35
+ const dialogRef = (0, react_1.useRef)(null);
36
+ const screenshotPreviewRef = (0, react_1.useRef)(null);
37
+ const savingOverlayRef = (0, react_1.useRef)(null);
38
+ const dismissRef = (0, react_1.useRef)(() => undefined);
39
+ const wasOpenRef = (0, react_1.useRef)(false);
40
+ const isOpen = flow.step !== 'closed';
41
+ const dismiss = () => {
42
+ if (flow.step !== 'success' && flow.step !== 'closed' && flow.submission) {
43
+ void onDismiss?.(flow.submission, flow.step, flow.completed)
44
+ ?.catch((error) => console.error('Feedback draft could not be saved on dismiss', error));
45
+ }
46
+ flow.close();
47
+ };
48
+ dismissRef.current = dismiss;
49
+ const isGapFirstFlow = flow.rating !== null && (flow.completed ? flow.ratingBase < 4 : flow.rating < 4);
50
+ const isReview = flow.completed && isOpen && flow.step !== 'success';
51
+ const isLowRatingGap = flow.step === 'gap' && flow.ratingBase <= 3 && !isReview;
52
+ const showLowRatingIntro = isLowRatingGap && !introDismissed;
53
+ const showPositiveRatingIntro = flow.step === 'time' && flow.ratingBase >= 4 && !isReview && !showUseful && !introDismissed;
54
+ const reviewPages = flow.ratingBase >= 4
55
+ ? [...(flow.ratingBase === 4 ? ['rating'] : []), 'time', 'gap']
56
+ : [...(flow.ratingBase === 3 ? ['rating'] : []), 'gap', 'useful'];
57
+ const reviewPage = reviewPages[reviewPageIndex];
58
+ const canContinueReview = reviewPage === 'gap'
59
+ ? flow.gaps.every((gap) => Boolean(gap.description.trim()) && Boolean(gap.severity))
60
+ : reviewPage === 'useful' || reviewPage === 'time'
61
+ ? Boolean(flow.mostUseful.trim()) &&
62
+ (reviewPage !== 'useful' || flow.timeSavedMinutes !== undefined)
63
+ : true;
64
+ (0, react_1.useEffect)(() => {
65
+ setCustomTime(false);
66
+ setIntroDismissed(false);
67
+ setShowUseful(false);
68
+ setShowLowRatingTime(false);
69
+ setRevealedGapCount(0);
70
+ setActiveGapIndex(0);
71
+ setReviewPageIndex(0);
72
+ setSubmitError(false);
73
+ }, [flow.requestId, flow.featureId, flow.rating]);
74
+ (0, react_1.useEffect)(() => {
75
+ if (isOpen && !wasOpenRef.current) {
76
+ setShowUseful((flow.rating ?? 0) >= 4 && flow.timeSavedMinutes !== undefined);
77
+ setShowLowRatingTime((flow.rating ?? 0) < 4 && flow.timeSavedMinutes !== undefined);
78
+ const firstShortGap = flow.gaps.findIndex((gap) => !gap.description.trim() || (wordCount(gap.description) < copy.usefulMinimumWords && !gap.detailRequested));
79
+ setRevealedGapCount(firstShortGap < 0 ? flow.gaps.length : firstShortGap);
80
+ setReviewPageIndex(0);
81
+ }
82
+ wasOpenRef.current = isOpen;
83
+ }, [isOpen]);
84
+ (0, react_1.useEffect)(() => {
85
+ if (!isReview || !reviewPage)
86
+ return;
87
+ const step = reviewPage === 'time' && isGapFirstFlow ? 'useful' : reviewPage;
88
+ if (flow.step !== step)
89
+ flow.setStep(step);
90
+ setShowUseful(reviewPage === 'time' && !isGapFirstFlow);
91
+ setShowLowRatingTime(reviewPage === 'useful' && isGapFirstFlow);
92
+ if (reviewPage === 'gap')
93
+ setRevealedGapCount(flow.gaps.length);
94
+ if (reviewPage === 'time')
95
+ setCustomTime(flow.timeSavedMinutes !== undefined && !copy.timeOptions.some((option) => option.value === flow.timeSavedMinutes));
96
+ }, [isReview, reviewPageIndex, reviewPages.join('|')]);
97
+ (0, react_1.useEffect)(() => {
98
+ if (isReview)
99
+ setReviewPageIndex(0);
100
+ }, [flow.ratingBase]);
101
+ (0, react_1.useLayoutEffect)(() => {
102
+ if (!isOpen)
103
+ return;
104
+ const updatePosition = () => {
105
+ const rect = anchorRef?.current?.getBoundingClientRect();
106
+ if (!rect)
107
+ return;
108
+ const panel = overlayHostRef?.current?.getBoundingClientRect() ?? anchorRef?.current
109
+ ?.closest('[data-feedback-chat-panel]')?.getBoundingClientRect();
110
+ const boundsTop = Math.max(0, panel?.top ?? 0);
111
+ const boundsBottom = Math.min(window.innerHeight, panel?.bottom ?? window.innerHeight);
112
+ const preferredWidth = flow.step === 'gap'
113
+ ? isLowRatingGap && flow.gaps.length === 1 && revealedGapCount === 0 ? 520 : 600
114
+ : flow.step === 'success' ? 620 : flow.step === 'time' ? 440 : 540;
115
+ const width = Math.min(preferredWidth, window.innerWidth - 24);
116
+ const above = Math.max(0, rect.top - boundsTop - 24);
117
+ const below = Math.max(0, boundsBottom - rect.bottom - 24);
118
+ const placeAbove = above >= below;
119
+ const placementMaxHeight = Math.max(80, placeAbove ? above : below);
120
+ const dialogHeight = Math.min(dialogRef.current?.scrollHeight ?? 340, placementMaxHeight);
121
+ const top = placeAbove
122
+ ? Math.max(boundsTop + 12, rect.top - dialogHeight - 12)
123
+ : Math.min(boundsBottom - dialogHeight - 12, rect.bottom + 12);
124
+ // Keep the dialog visually attached to the rating card that opened it.
125
+ // Clamp to the viewport, not the chat panel, so narrow panels can still show it fully.
126
+ const left = Math.max(12, Math.min(window.innerWidth - width - 12, rect.right - width));
127
+ setPosition((previous) => previous.left === left && previous.top === top && previous.width === width && previous.maxHeight === placementMaxHeight
128
+ ? previous
129
+ : { left, top, width, maxHeight: placementMaxHeight });
130
+ };
131
+ updatePosition();
132
+ window.addEventListener('resize', updatePosition);
133
+ window.addEventListener('scroll', updatePosition, true);
134
+ const panel = overlayHostRef?.current ?? anchorRef?.current?.closest('[data-feedback-chat-panel]');
135
+ const resizeObserver = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(updatePosition) : null;
136
+ if (panel)
137
+ resizeObserver?.observe(panel);
138
+ if (dialogRef.current)
139
+ resizeObserver?.observe(dialogRef.current);
140
+ if (anchorRef?.current)
141
+ resizeObserver?.observe(anchorRef?.current);
142
+ return () => {
143
+ window.removeEventListener('resize', updatePosition);
144
+ window.removeEventListener('scroll', updatePosition, true);
145
+ resizeObserver?.disconnect();
146
+ };
147
+ }, [anchorRef, overlayHostRef, anchorKey, isOpen, flow.step, flow.gaps.length, revealedGapCount, showLowRatingTime, showUseful, isLowRatingGap, showPositiveRatingIntro]);
148
+ (0, react_1.useEffect)(() => {
149
+ if (!isOpen)
150
+ return;
151
+ const onKeyDown = (event) => {
152
+ if (event.key !== 'Escape' || isSubmitting || saving || capturingScreenshot)
153
+ return;
154
+ if (screenshotPreview) {
155
+ event.preventDefault();
156
+ setScreenshotPreview(null);
157
+ }
158
+ else
159
+ dismissRef.current();
160
+ };
161
+ window.addEventListener('keydown', onKeyDown);
162
+ dialogRef.current?.focus();
163
+ return () => window.removeEventListener('keydown', onKeyDown);
164
+ }, [isOpen, flow.setStep, isSubmitting, saving, screenshotPreview, capturingScreenshot]);
165
+ (0, react_1.useEffect)(() => {
166
+ if (screenshotPreview)
167
+ screenshotPreviewRef.current?.focus();
168
+ }, [screenshotPreview]);
169
+ (0, react_1.useEffect)(() => {
170
+ if (isSubmitting || saving)
171
+ savingOverlayRef.current?.focus();
172
+ }, [isSubmitting, saving]);
173
+ if (!isOpen)
174
+ return null;
175
+ const busy = isSubmitting || saving;
176
+ const ratingBase = flow.ratingBase;
177
+ const decimalRatings = Array.from({ length: 6 }, (_, index) => Number((ratingBase + index * 0.2).toFixed(1)));
178
+ const updateGap = (index, patch) => {
179
+ flow.setGaps(flow.gaps.map((gap, gapIndex) => (gapIndex === index ? { ...gap, ...patch } : gap)));
180
+ };
181
+ const removeGap = (index) => {
182
+ if (flow.gaps.length < 2)
183
+ return;
184
+ flow.setGaps(flow.gaps.filter((_, gapIndex) => gapIndex !== index));
185
+ setRevealedGapCount((count) => Math.min(flow.gaps.length - 1, count - (index < count ? 1 : 0)));
186
+ setActiveGapIndex((current) => current === index
187
+ ? Math.max(0, Math.min(index, flow.gaps.length - 2))
188
+ : current > index ? current - 1 : current);
189
+ };
190
+ const canAdvanceGap = revealedGapCount >= flow.gaps.length && flow.gaps.every((gap) => Boolean(gap.severity) && Boolean(gap.description.trim()) &&
191
+ (wordCount(gap.description) >= copy.usefulMinimumWords || gap.detailRequested));
192
+ const persistAndAdvance = (nextStep, advance, submission = flow.submission) => {
193
+ if (!submission || submittingRef.current)
194
+ return;
195
+ setSubmitError(false);
196
+ advance();
197
+ // Progress saves are serialized by useFeedbackPersistence. Navigation must not
198
+ // wait for the network; the final submit waits for every queued save.
199
+ void Promise.resolve()
200
+ .then(() => onProgress?.(submission, nextStep))
201
+ .catch((error) => {
202
+ console.error('Feedback progress could not be saved', error);
203
+ });
204
+ };
205
+ const submitFeedback = async () => {
206
+ if (!flow.submission || submittingRef.current)
207
+ return;
208
+ if (!flow.mostUseful.trim() ||
209
+ (wordCount(flow.mostUseful) < copy.usefulMinimumWords && !flow.usefulDetailShown) ||
210
+ flow.gaps.some((gap) => !gap.description.trim() ||
211
+ (wordCount(gap.description) < copy.usefulMinimumWords && !gap.detailRequested)) ||
212
+ flow.timeSavedMinutes === undefined)
213
+ return;
214
+ submittingRef.current = true;
215
+ setIsSubmitting(true);
216
+ try {
217
+ setSubmitError(false);
218
+ await onSubmit?.(flow.submission);
219
+ flow.setCompleted(true);
220
+ flow.setStep('success');
221
+ }
222
+ catch (error) {
223
+ console.error('Feedback submission failed', error);
224
+ setSubmitError(true);
225
+ }
226
+ finally {
227
+ submittingRef.current = false;
228
+ setIsSubmitting(false);
229
+ }
230
+ };
231
+ const nextReviewPage = () => {
232
+ if (!canContinueReview)
233
+ return;
234
+ if (reviewPage === 'gap') {
235
+ const gaps = flow.gaps.map((gap) => wordCount(gap.description) < copy.usefulMinimumWords
236
+ ? { ...gap, detailRequested: true } : gap);
237
+ if (gaps.some((gap, index) => gap.detailRequested && !flow.gaps[index].detailRequested)) {
238
+ flow.setGaps(gaps);
239
+ return;
240
+ }
241
+ }
242
+ else if ((reviewPage === 'useful' || reviewPage === 'time') &&
243
+ wordCount(flow.mostUseful) < copy.usefulMinimumWords && !flow.usefulDetailShown) {
244
+ flow.setUsefulDetailShown(true);
245
+ return;
246
+ }
247
+ if (reviewPageIndex < reviewPages.length - 1) {
248
+ const nextPage = reviewPages[reviewPageIndex + 1];
249
+ void persistAndAdvance(nextPage, () => setReviewPageIndex(reviewPageIndex + 1));
250
+ }
251
+ else
252
+ void submitFeedback();
253
+ };
254
+ const previousReviewPage = () => {
255
+ if (reviewPageIndex > 0)
256
+ setReviewPageIndex(reviewPageIndex - 1);
257
+ };
258
+ const continueUseful = () => {
259
+ if (!flow.mostUseful.trim())
260
+ return;
261
+ if (wordCount(flow.mostUseful) < copy.usefulMinimumWords && !flow.usefulDetailShown) {
262
+ flow.setUsefulDetailShown(true);
263
+ return;
264
+ }
265
+ if (isReview) {
266
+ nextReviewPage();
267
+ return;
268
+ }
269
+ if (flow.timeSavedMinutes !== undefined)
270
+ void submitFeedback();
271
+ };
272
+ const continueTime = () => {
273
+ if (showUseful && !flow.mostUseful.trim())
274
+ return;
275
+ if (showUseful && wordCount(flow.mostUseful) < copy.usefulMinimumWords && !flow.usefulDetailShown) {
276
+ flow.setUsefulDetailShown(true);
277
+ return;
278
+ }
279
+ if (isReview) {
280
+ nextReviewPage();
281
+ return;
282
+ }
283
+ if (flow.timeSavedMinutes === undefined)
284
+ return;
285
+ if (showUseful) {
286
+ void persistAndAdvance('gap', () => flow.setStep('gap'));
287
+ }
288
+ else
289
+ void persistAndAdvance('time', () => setShowUseful(true));
290
+ };
291
+ const selectDecimalRating = (rating) => {
292
+ flow.setRating(rating);
293
+ const nextStep = rating < 4 ? 'gap' : 'time';
294
+ void persistAndAdvance(nextStep, () => flow.setStep(nextStep), {
295
+ requestId: flow.requestId,
296
+ featureId: flow.featureId,
297
+ rating,
298
+ gaps: [],
299
+ metadata: flow.submission?.metadata,
300
+ });
301
+ };
302
+ const selectTimeSaved = (minutes) => {
303
+ flow.setTimeSavedMinutes(minutes);
304
+ if (!flow.submission)
305
+ return;
306
+ void persistAndAdvance(isGapFirstFlow ? 'useful' : 'time', () => {
307
+ if (isGapFirstFlow)
308
+ setShowLowRatingTime(true);
309
+ else
310
+ setShowUseful(true);
311
+ }, { ...flow.submission, timeSavedMinutes: minutes });
312
+ };
313
+ const returnToRating = () => {
314
+ flow.setTimeSavedMinutes(undefined);
315
+ flow.setMostUseful('');
316
+ flow.setUsefulMoreDetail('');
317
+ flow.setUsefulDetailShown(false);
318
+ flow.setGaps([{ description: '', screenshots: [] }]);
319
+ setShowUseful(false);
320
+ setShowLowRatingTime(false);
321
+ setCustomTime(false);
322
+ setRevealedGapCount(0);
323
+ setActiveGapIndex(0);
324
+ flow.setStep('rating');
325
+ };
326
+ const back = () => {
327
+ if (flow.step === 'time') {
328
+ returnToRating();
329
+ }
330
+ else if (flow.step === 'gap') {
331
+ if (flow.rating !== null && flow.rating < 4)
332
+ returnToRating();
333
+ else {
334
+ setShowUseful(true);
335
+ flow.setStep('time');
336
+ }
337
+ }
338
+ else if (flow.step === 'useful' && isGapFirstFlow) {
339
+ if (showLowRatingTime)
340
+ setShowLowRatingTime(false);
341
+ else
342
+ flow.setStep('gap');
343
+ }
344
+ };
345
+ const captureTabScreenshot = async (gapIndex) => {
346
+ if (!navigator.mediaDevices?.getDisplayMedia) {
347
+ setCaptureError('Tab capture is not supported in this browser. Please attach a screenshot instead.');
348
+ return;
349
+ }
350
+ let stream;
351
+ let video;
352
+ try {
353
+ setCaptureError('');
354
+ // Call from the click itself: browsers require a user gesture for display capture.
355
+ const capturePromise = navigator.mediaDevices.getDisplayMedia({
356
+ video: { displaySurface: 'browser' },
357
+ audio: false,
358
+ preferCurrentTab: true,
359
+ });
360
+ (0, react_dom_1.flushSync)(() => setCapturingScreenshot(true));
361
+ document.body.classList.add('cw-feedback-capturing');
362
+ stream = await capturePromise;
363
+ const track = stream.getVideoTracks()[0];
364
+ const displaySurface = track?.getSettings().displaySurface;
365
+ if (!track || (displaySurface && displaySurface !== 'browser')) {
366
+ throw new Error('Please select a browser tab to capture.');
367
+ }
368
+ video = document.createElement('video');
369
+ video.srcObject = stream;
370
+ video.muted = true;
371
+ await video.play();
372
+ await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
373
+ const canvas = document.createElement('canvas');
374
+ canvas.width = video.videoWidth || track.getSettings().width || 0;
375
+ canvas.height = video.videoHeight || track.getSettings().height || 0;
376
+ if (!canvas.width || !canvas.height)
377
+ throw new Error('The selected tab did not provide a video frame.');
378
+ const context = canvas.getContext('2d');
379
+ if (!context)
380
+ throw new Error('Could not capture the selected tab.');
381
+ context.drawImage(video, 0, 0);
382
+ const blob = await new Promise((resolve) => canvas.toBlob(resolve, 'image/png'));
383
+ if (!blob)
384
+ throw new Error('Could not create the screenshot.');
385
+ const file = new File([blob], `feedback-screenshot-${Date.now()}.png`, { type: 'image/png' });
386
+ flow.setGaps((gaps) => gaps.map((gap, index) => index === gapIndex ? { ...gap, screenshots: [...gap.screenshots, file] } : gap));
387
+ }
388
+ catch (error) {
389
+ if (error instanceof Error && error.name !== 'NotAllowedError' && error.name !== 'AbortError') {
390
+ setCaptureError(error.message);
391
+ }
392
+ }
393
+ finally {
394
+ stream?.getTracks().forEach((track) => track.stop());
395
+ if (video)
396
+ video.srcObject = null;
397
+ document.body.classList.remove('cw-feedback-capturing');
398
+ setCapturingScreenshot(false);
399
+ }
400
+ };
401
+ const backdrop = ((0, jsx_runtime_1.jsx)("div", { onMouseDown: (event) => !busy && event.target === event.currentTarget && dismiss(), className: ['cw-feedback-backdrop', backdropClassName, classNames?.backdrop].filter(Boolean).join(' '), style: {
402
+ background: mergedTheme.overlay ?? '#11182755',
403
+ visibility: capturingScreenshot ? 'hidden' : undefined,
404
+ ...backdropStyle,
405
+ ...styles?.backdrop,
406
+ } }));
407
+ const dialog = ((0, jsx_runtime_1.jsxs)("div", { ref: dialogRef, style: {
408
+ ...position,
409
+ ...(0, feedbackTheme_1.feedbackThemeStyle)(theme),
410
+ visibility: capturingScreenshot ? 'hidden' : undefined,
411
+ ...style,
412
+ ...dialogStyle,
413
+ ...styles?.dialog,
414
+ }, className: [
415
+ 'cw-feedback-dialog',
416
+ flow.step === 'gap' ? 'cw-feedback-dialog--wide' : '',
417
+ isLowRatingGap && flow.gaps.length === 1 && revealedGapCount === 0 ? 'cw-feedback-dialog--low-intro' : '',
418
+ flow.step === 'gap' && canAdvanceGap ? 'cw-feedback-dialog--has-action' : '',
419
+ flow.step === 'success' ? 'cw-feedback-dialog--success' : '',
420
+ className,
421
+ dialogClassName,
422
+ classNames?.dialog,
423
+ ]
424
+ .filter(Boolean)
425
+ .join(' '), role: "dialog", "aria-modal": "true", "aria-busy": busy, tabIndex: -1, onKeyDown: (event) => {
426
+ if (busy) {
427
+ event.preventDefault();
428
+ return;
429
+ }
430
+ if (event.target instanceof HTMLButtonElement)
431
+ return;
432
+ if (isReview && event.key === 'Enter' && !(event.target instanceof HTMLTextAreaElement) && !(event.target instanceof HTMLInputElement && event.target.type === 'number')) {
433
+ event.preventDefault();
434
+ nextReviewPage();
435
+ return;
436
+ }
437
+ if (event.key === 'Enter' && flow.step === 'rating' && flow.rating !== null) {
438
+ event.preventDefault();
439
+ selectDecimalRating(flow.rating);
440
+ }
441
+ if (event.key === 'Enter' &&
442
+ flow.step === 'time' &&
443
+ flow.timeSavedMinutes !== undefined &&
444
+ !showUseful &&
445
+ event.target instanceof HTMLElement &&
446
+ event.target.tagName !== 'TEXTAREA' &&
447
+ !(event.target instanceof HTMLInputElement && event.target.type === 'number')) {
448
+ event.preventDefault();
449
+ continueTime();
450
+ }
451
+ if (event.key === 'Enter' &&
452
+ flow.step === 'useful' &&
453
+ showLowRatingTime &&
454
+ flow.timeSavedMinutes !== undefined &&
455
+ event.target instanceof HTMLElement &&
456
+ event.target.tagName !== 'TEXTAREA') {
457
+ event.preventDefault();
458
+ continueUseful();
459
+ }
460
+ }, children: [(0, jsx_runtime_1.jsxs)("div", { className: ['cw-feedback-dialog__content', classNames?.content].filter(Boolean).join(' '), style: styles?.content, children: [!isReview &&
461
+ ((flow.step === 'time' && flow.ratingBase === 4) ||
462
+ (flow.step === 'gap' && flow.rating !== null && flow.rating > 2) ||
463
+ (flow.step === 'useful' && isGapFirstFlow) ||
464
+ flow.step === 'success') && ((0, jsx_runtime_1.jsxs)("div", { className: ['cw-feedback-dialog__topbar', classNames?.topbar].filter(Boolean).join(' '), style: styles?.topbar, children: [flow.step === 'time' || flow.step === 'gap' || flow.step === 'useful' ? ((0, jsx_runtime_1.jsx)("button", { type: "button", onClick: back, "aria-label": copy.backLabel, className: classNames?.backButton, style: styles?.backButton, children: (0, jsx_runtime_1.jsx)(lucide_react_1.ArrowLeft, { size: 19 }) })) : ((0, jsx_runtime_1.jsx)("span", {})), flow.step === 'success' && ((0, jsx_runtime_1.jsx)("button", { type: "button", onClick: flow.close, "aria-label": copy.closeLabel, className: classNames?.closeButton, style: styles?.closeButton, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 19 }) }))] })), flow.step === 'rating' && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("h2", { className: classNames?.title, style: styles?.title, children: copy.ratingTitle }), (0, jsx_runtime_1.jsxs)("p", { className: ['cw-feedback-subtitle', classNames?.subtitle].filter(Boolean).join(' '), style: styles?.subtitle, children: ["\u2728 ", copy.ratingSubtitle] }), (0, jsx_runtime_1.jsxs)("div", { className: ['cw-feedback-range', classNames?.rangeContainer].filter(Boolean).join(' '), style: styles?.rangeContainer, children: [flow.rating !== null && ((0, jsx_runtime_1.jsx)("div", { className: ['cw-feedback-range-tooltip', classNames?.rangeTooltip].filter(Boolean).join(' '), style: {
465
+ left: `clamp(150px, ${(flow.rating - ratingBase) * 100}%, calc(100% - 150px))`,
466
+ ...styles?.rangeTooltip,
467
+ }, children: copy.ratingValueLabel(flow.rating) })), (0, jsx_runtime_1.jsx)("input", { "aria-label": copy.ratingControlLabel, type: "range", min: ratingBase, max: ratingBase + 1, step: "0.1", value: flow.rating ?? ratingBase, className: classNames?.rangeInput, style: styles?.rangeInput, onChange: (e) => (isReview ? flow.setReviewRating : flow.setRating)(Number(e.target.value)), onPointerUp: (event) => selectDecimalRating(Number(event.currentTarget.value)), onKeyUp: (event) => {
468
+ if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key)) {
469
+ selectDecimalRating(Number(event.currentTarget.value));
470
+ }
471
+ }, onKeyDown: (event) => {
472
+ if (event.key === 'Enter') {
473
+ event.preventDefault();
474
+ event.stopPropagation();
475
+ selectDecimalRating(Number(event.currentTarget.value));
476
+ }
477
+ } })] }), (0, jsx_runtime_1.jsx)("div", { className: ['cw-feedback-rating-options', classNames?.ratingOptionsContainer].filter(Boolean).join(' '), style: styles?.ratingOptionsContainer, children: decimalRatings.map((value) => {
478
+ const isSelected = flow.rating === value;
479
+ return ((0, jsx_runtime_1.jsx)("button", { type: "button", className: [
480
+ isSelected ? 'is-selected' : '',
481
+ classNames?.ratingOptionButton,
482
+ isSelected ? classNames?.ratingOptionButtonSelected : '',
483
+ ]
484
+ .filter(Boolean)
485
+ .join(' '), style: {
486
+ ...styles?.ratingOptionButton,
487
+ ...(isSelected ? styles?.ratingOptionButtonSelected : {}),
488
+ }, onClick: () => selectDecimalRating(value), children: value.toFixed(1) }, value));
489
+ }) }), (0, jsx_runtime_1.jsx)("p", { className: ['cw-feedback-hint', classNames?.hint].filter(Boolean).join(' '), style: styles?.hint, children: copy.ratingHint }), slots?.rating] })), flow.step === 'time' && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [showPositiveRatingIntro && ((0, jsx_runtime_1.jsxs)("div", { className: "cw-feedback-low-rating-intro", children: [(0, jsx_runtime_1.jsx)("div", { className: "cw-feedback-low-rating-intro__emoji", "aria-hidden": "true", children: "\uD83D\uDE0A\uD83E\uDD73" }), (0, jsx_runtime_1.jsx)("strong", { children: copy.positiveRatingIntroTitle }), (0, jsx_runtime_1.jsx)("p", { children: copy.positiveRatingIntroDescription })] })), (0, jsx_runtime_1.jsx)("h2", { className: classNames?.title, style: styles?.title, children: copy.timeSavedTitle }), (0, jsx_runtime_1.jsxs)("div", { className: ['cw-feedback-pills', 'cw-feedback-time-options', classNames?.timePillsContainer].filter(Boolean).join(' '), style: styles?.timePillsContainer, children: [copy.timeOptions.map(({ value, label }) => {
490
+ const isSelected = flow.timeSavedMinutes === value && !customTime;
491
+ return ((0, jsx_runtime_1.jsx)("button", { className: [
492
+ isSelected ? 'is-selected' : '',
493
+ classNames?.timePillButton,
494
+ isSelected ? classNames?.timePillButtonSelected : '',
495
+ ]
496
+ .filter(Boolean)
497
+ .join(' '), style: {
498
+ ...styles?.timePillButton,
499
+ ...(isSelected ? styles?.timePillButtonSelected : {}),
500
+ }, type: "button", onClick: () => {
501
+ setIntroDismissed(true);
502
+ setCustomTime(false);
503
+ selectTimeSaved(value);
504
+ }, children: label }, value));
505
+ }), (0, jsx_runtime_1.jsx)("button", { className: [
506
+ customTime ? 'is-selected' : '',
507
+ classNames?.timePillButton,
508
+ customTime ? classNames?.timePillButtonSelected : '',
509
+ ]
510
+ .filter(Boolean)
511
+ .join(' '), style: {
512
+ ...styles?.timePillButton,
513
+ ...(customTime ? styles?.timePillButtonSelected : {}),
514
+ }, type: "button", onClick: () => { setIntroDismissed(true); setCustomTime(true); }, children: copy.otherLabel }), customTime && ((0, jsx_runtime_1.jsxs)("label", { className: ['cw-feedback-number', 'cw-feedback-time-custom', classNames?.customTimeWrapper].filter(Boolean).join(' '), style: styles?.customTimeWrapper, children: [(0, jsx_runtime_1.jsx)("input", { autoFocus: true, type: "number", min: "0", value: flow.timeSavedMinutes ?? '', className: classNames?.customTimeInput, style: styles?.customTimeInput, onChange: (e) => flow.setTimeSavedMinutes(Number(e.target.value)), onKeyDown: (event) => {
515
+ if (event.key !== 'Enter' || flow.timeSavedMinutes === undefined)
516
+ return;
517
+ event.preventDefault();
518
+ event.stopPropagation();
519
+ selectTimeSaved(flow.timeSavedMinutes);
520
+ } }), ' ', copy.minuteUnit] }))] }), !showUseful && flow.timeSavedMinutes === undefined && ((0, jsx_runtime_1.jsx)("p", { className: "cw-feedback-time-hint", children: copy.enterHint })), showUseful && ((0, jsx_runtime_1.jsxs)("label", { className: ['cw-feedback-field', classNames?.usefulField].filter(Boolean).join(' '), style: styles?.usefulField, children: [(0, jsx_runtime_1.jsx)("span", { children: copy.usefulTitle }), flow.usefulDetailShown && wordCount(flow.mostUseful) < copy.usefulMinimumWords && ((0, jsx_runtime_1.jsxs)("span", { className: "cw-feedback-inline-detail-prompt", children: ["*", copy.usefulMoreDetailPrompt] })), (0, jsx_runtime_1.jsx)("textarea", { autoFocus: true, value: flow.mostUseful, className: classNames?.usefulTextarea, style: styles?.usefulTextarea, onChange: (e) => flow.setMostUseful(e.target.value), placeholder: copy.usefulPlaceholder, onKeyDown: (e) => {
521
+ if (e.key === 'Enter' && !e.shiftKey && flow.mostUseful.trim()) {
522
+ e.preventDefault();
523
+ if (isReview)
524
+ nextReviewPage();
525
+ else
526
+ continueTime();
527
+ }
528
+ } })] })), !isReview && flow.timeSavedMinutes !== undefined &&
529
+ (!showUseful || !flow.usefulDetailShown || Boolean(flow.mostUseful.trim())) && ((0, jsx_runtime_1.jsx)("div", { className: ['cw-feedback-footer', classNames?.footer].filter(Boolean).join(' '), style: styles?.footer, children: (0, jsx_runtime_1.jsx)("button", { className: ['cw-feedback-hint-action', classNames?.enterHintButton].filter(Boolean).join(' '), style: styles?.enterHintButton, type: "button", onClick: continueTime, children: copy.enterHint }) })), slots?.time] })), flow.step === 'useful' && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("h2", { className: ['cw-feedback-time-title', classNames?.title].filter(Boolean).join(' '), style: styles?.title, children: copy.timeSavedTitle }), (0, jsx_runtime_1.jsxs)("div", { className: ['cw-feedback-pills', classNames?.timePillsContainer].filter(Boolean).join(' '), style: styles?.timePillsContainer, children: [copy.timeOptions.map(({ value, label }) => {
530
+ const isSelected = flow.timeSavedMinutes === value && !customTime;
531
+ return ((0, jsx_runtime_1.jsx)("button", { className: [
532
+ isSelected ? 'is-selected' : '',
533
+ classNames?.timePillButton,
534
+ isSelected ? classNames?.timePillButtonSelected : '',
535
+ ]
536
+ .filter(Boolean)
537
+ .join(' '), style: {
538
+ ...styles?.timePillButton,
539
+ ...(isSelected ? styles?.timePillButtonSelected : {}),
540
+ }, type: "button", onClick: () => {
541
+ setCustomTime(false);
542
+ selectTimeSaved(value);
543
+ }, children: label }, value));
544
+ }), (0, jsx_runtime_1.jsx)("button", { className: [
545
+ customTime ? 'is-selected' : '',
546
+ classNames?.timePillButton,
547
+ customTime ? classNames?.timePillButtonSelected : '',
548
+ ]
549
+ .filter(Boolean)
550
+ .join(' '), style: {
551
+ ...styles?.timePillButton,
552
+ ...(customTime ? styles?.timePillButtonSelected : {}),
553
+ }, type: "button", onClick: () => setCustomTime(true), children: copy.otherLabel })] }), customTime && ((0, jsx_runtime_1.jsxs)("label", { className: ['cw-feedback-number', classNames?.customTimeWrapper].filter(Boolean).join(' '), style: styles?.customTimeWrapper, children: [(0, jsx_runtime_1.jsx)("input", { autoFocus: true, type: "number", min: "0", value: flow.timeSavedMinutes ?? '', className: classNames?.customTimeInput, style: styles?.customTimeInput, onChange: (event) => flow.setTimeSavedMinutes(Number(event.target.value)), onKeyDown: (event) => {
554
+ if (event.key !== 'Enter' || flow.timeSavedMinutes === undefined)
555
+ return;
556
+ event.preventDefault();
557
+ event.stopPropagation();
558
+ selectTimeSaved(flow.timeSavedMinutes);
559
+ } }), ' ', copy.minuteUnit] })), showLowRatingTime && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("h2", { className: ['cw-feedback-useful-title', classNames?.title, classNames?.usefulTitle].filter(Boolean).join(' '), style: { ...styles?.title, ...styles?.usefulTitle }, children: copy.lowRatingUsefulTitle }), flow.usefulDetailShown && wordCount(flow.mostUseful) < copy.usefulMinimumWords && ((0, jsx_runtime_1.jsxs)("p", { className: ['cw-feedback-useful-prompt', classNames?.usefulPrompt].filter(Boolean).join(' '), style: styles?.usefulPrompt, children: ["*", copy.usefulMoreDetailPrompt] })), (0, jsx_runtime_1.jsx)("label", { className: ['cw-feedback-field', 'cw-feedback-useful-field', classNames?.usefulField].filter(Boolean).join(' '), style: styles?.usefulField, children: (0, jsx_runtime_1.jsx)("textarea", { autoFocus: true, value: flow.mostUseful, className: classNames?.usefulTextarea, style: styles?.usefulTextarea, onChange: (event) => flow.setMostUseful(event.target.value), placeholder: copy.usefulPlaceholder, onKeyDown: (event) => {
560
+ if (event.key !== 'Enter' || event.shiftKey)
561
+ return;
562
+ event.preventDefault();
563
+ continueUseful();
564
+ } }) })] }))] })), flow.step === 'gap' && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [showLowRatingIntro && ((0, jsx_runtime_1.jsxs)("div", { className: "cw-feedback-low-rating-intro", children: [(0, jsx_runtime_1.jsx)("div", { className: "cw-feedback-low-rating-intro__emoji", "aria-hidden": "true", children: "\uD83D\uDE14\uD83E\uDD7A" }), (0, jsx_runtime_1.jsx)("strong", { children: copy.lowRatingIntroTitle }), (0, jsx_runtime_1.jsx)("p", { children: copy.lowRatingIntroDescription })] })), (0, jsx_runtime_1.jsx)("h2", { className: classNames?.title, style: styles?.title, children: flow.rating === 5 ? copy.fiveStarGapTitle : copy.gapTitle }), copy.gapSubtitle && (!isLowRatingGap || (flow.gaps.length === 1 && flow.gaps[0].detailRequested)) && ((0, jsx_runtime_1.jsx)("p", { className: [
565
+ flow.gaps.length === 1 && flow.gaps[0].detailRequested && wordCount(flow.gaps[0].description) < copy.usefulMinimumWords
566
+ ? 'cw-feedback-inline-detail-prompt' : 'cw-feedback-subtitle',
567
+ classNames?.subtitle,
568
+ ].filter(Boolean).join(' '), style: styles?.subtitle, children: flow.gaps.length === 1 && flow.gaps[0].detailRequested && wordCount(flow.gaps[0].description) < copy.usefulMinimumWords
569
+ ? `*${copy.gapMoreDetailPrompt}`
570
+ : `✨ ${copy.gapSubtitle}` })), (0, jsx_runtime_1.jsx)("div", { className: ['cw-feedback-gap-list', classNames?.gapList].filter(Boolean).join(' '), style: styles?.gapList, children: flow.gaps.map((gap, index) => ((0, jsx_runtime_1.jsxs)("section", { className: ['cw-feedback-gap', flow.gaps.length >= 2 && index !== activeGapIndex ? 'cw-feedback-gap--collapsed' : '', classNames?.gapItem].filter(Boolean).join(' '), style: styles?.gapItem, children: [flow.gaps.length >= 2 && ((0, jsx_runtime_1.jsxs)("div", { className: "cw-feedback-gap__header", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "cw-feedback-gap__toggle", onClick: () => setActiveGapIndex(index), "aria-expanded": index === activeGapIndex, children: ["Gap ", index + 1, gap.severity && (0, jsx_runtime_1.jsx)("span", { className: `cw-feedback-gap__severity ${gap.severity}`, children: copy.severityLabels[gap.severity] }), index !== activeGapIndex && ((0, jsx_runtime_1.jsx)("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)("path", { d: "m6 9 6 6 6-6" }) }))] }), index === activeGapIndex && (0, jsx_runtime_1.jsx)("button", { type: "button", className: "cw-feedback-gap__remove", onClick: () => removeGap(index), "aria-label": `Delete gap ${index + 1}`, title: `Delete gap ${index + 1}`, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 16, "aria-hidden": "true" }) })] })), (flow.gaps.length < 2 || index === activeGapIndex) && (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [flow.gaps.length >= 2 && gap.detailRequested && wordCount(gap.description) < copy.usefulMinimumWords && ((0, jsx_runtime_1.jsxs)("p", { className: "cw-feedback-inline-detail-prompt cw-feedback-gap__detail-prompt", children: ["*", copy.gapMoreDetailPrompt] })), (0, jsx_runtime_1.jsx)("textarea", { autoFocus: index === 0 && !isLowRatingGap, value: gap.description, className: classNames?.gapTextarea, style: styles?.gapTextarea, onFocus: () => { if (isLowRatingGap)
571
+ setIntroDismissed(true); }, onChange: (e) => updateGap(index, { description: e.target.value }), onKeyDown: (event) => {
572
+ if (event.key === 'Enter' && !event.shiftKey && gap.description.trim()) {
573
+ event.preventDefault();
574
+ if (wordCount(gap.description) < copy.usefulMinimumWords && !gap.detailRequested) {
575
+ updateGap(index, { detailRequested: true });
576
+ return;
577
+ }
578
+ setRevealedGapCount(Math.max(revealedGapCount, index + 1));
579
+ }
580
+ }, placeholder: copy.gapPlaceholder }), index < revealedGapCount && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("strong", { className: classNames?.severityTitle, style: styles?.severityTitle, children: copy.severityQuestion }), (0, jsx_runtime_1.jsx)("div", { className: ['cw-feedback-severity', classNames?.severityContainer].filter(Boolean).join(' '), style: styles?.severityContainer, children: severities.map((severity) => {
581
+ const isSelected = gap.severity === severity;
582
+ return ((0, jsx_runtime_1.jsx)("button", { type: "button", className: [severity, isSelected ? 'is-selected' : '', classNames?.severityButton]
583
+ .filter(Boolean)
584
+ .join(' '), style: {
585
+ color: mergedTheme.severityColors?.[severity],
586
+ '--cw-feedback-severity-color': mergedTheme.severityColors?.[severity],
587
+ ...(isSelected
588
+ ? {
589
+ ...(mergedTheme.severityBackgrounds?.[severity]
590
+ ? { backgroundColor: mergedTheme.severityBackgrounds[severity] }
591
+ : {}),
592
+ }
593
+ : {}),
594
+ ...styles?.severityButton,
595
+ }, onClick: () => {
596
+ const gaps = flow.gaps.map((item, gapIndex) => gapIndex === index ? { ...item, severity } : item);
597
+ flow.setGaps(gaps);
598
+ if (flow.submission) {
599
+ void persistAndAdvance('gap', () => undefined, {
600
+ ...flow.submission,
601
+ gaps: gaps.filter((item) => item.description.trim() || item.screenshots.length),
602
+ });
603
+ }
604
+ }, children: copy.severityLabels[severity] }, severity));
605
+ }) }), (0, jsx_runtime_1.jsxs)("div", { className: "cw-feedback-capture-actions", children: [(0, jsx_runtime_1.jsxs)("label", { className: ['cw-feedback-upload', classNames?.uploadButton].filter(Boolean).join(' '), style: styles?.uploadButton, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Paperclip, { size: 18 }), " ", copy.attachScreenshot, (0, jsx_runtime_1.jsx)("input", { type: "file", accept: "image/*", multiple: true, className: classNames?.uploadInput, style: styles?.uploadInput, onChange: (e) => {
606
+ const files = Array.from(e.target.files ?? []);
607
+ if (files.length)
608
+ updateGap(index, { screenshots: [...gap.screenshots, ...files] });
609
+ e.target.value = '';
610
+ } })] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "cw-feedback-upload", onClick: () => void captureTabScreenshot(index), children: [(0, jsx_runtime_1.jsx)("span", { "aria-hidden": "true", children: "\u25A3" }), " Take a screenshot"] })] }), captureError && (0, jsx_runtime_1.jsx)("p", { className: "cw-feedback-capture-error", role: "alert", children: captureError }), gap.screenshots.length > 0 && ((0, jsx_runtime_1.jsx)("div", { className: ['cw-feedback-files', classNames?.filesList].filter(Boolean).join(' '), style: styles?.filesList, children: gap.screenshots.map((file, fileIndex) => ((0, jsx_runtime_1.jsx)("div", { className: classNames?.fileTag, style: styles?.fileTag, children: (0, jsx_runtime_1.jsx)(FeedbackScreenshotThumbnail_1.FeedbackScreenshotThumbnail, { screenshot: file, resolveUrl: resolveScreenshotUrl, onOpen: (url, name) => setScreenshotPreview({ url, name }), onRemove: () => updateGap(index, { screenshots: gap.screenshots.filter((_, screenshotIndex) => screenshotIndex !== fileIndex) }) }) }, `${file.name}-${file.lastModified}-${fileIndex}`))) }))] }))] })] }, index))) }), revealedGapCount < flow.gaps.length &&
611
+ (!isLowRatingGap || Boolean(flow.gaps[revealedGapCount]?.description.trim())) &&
612
+ (!flow.gaps[revealedGapCount]?.detailRequested ||
613
+ Boolean(flow.gaps[revealedGapCount]?.description.trim())) && ((0, jsx_runtime_1.jsx)("button", { className: ['cw-feedback-hint', 'cw-feedback-hint-action', classNames?.hint, classNames?.enterHintButton]
614
+ .filter(Boolean)
615
+ .join(' '), style: {
616
+ ...styles?.hint,
617
+ ...styles?.enterHintButton,
618
+ }, type: "button", onClick: () => {
619
+ const index = revealedGapCount;
620
+ const gap = flow.gaps[index];
621
+ if (!gap?.description.trim())
622
+ return;
623
+ if (wordCount(gap.description) < copy.usefulMinimumWords && !gap.detailRequested) {
624
+ updateGap(index, { detailRequested: true });
625
+ return;
626
+ }
627
+ setRevealedGapCount(index + 1);
628
+ }, children: copy.enterHint })), canAdvanceGap && ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: (0, jsx_runtime_1.jsxs)("button", { className: ['cw-feedback-add', classNames?.addGapButton].filter(Boolean).join(' '), style: styles?.addGapButton, type: "button", onClick: () => {
629
+ setActiveGapIndex(flow.gaps.length);
630
+ flow.setGaps([...flow.gaps, { description: '', screenshots: [] }]);
631
+ }, children: ["\uFF0B ", copy.addAnotherGap] }) })), slots?.gap] })), flow.step === 'success' && ((0, jsx_runtime_1.jsxs)("div", { className: ['cw-feedback-success', classNames?.successContainer].filter(Boolean).join(' '), style: styles?.successContainer, children: [(0, jsx_runtime_1.jsx)("span", { className: ['cw-feedback-success__check', classNames?.successCheck].filter(Boolean).join(' '), style: styles?.successCheck, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Check, { size: 38 }) }), (0, jsx_runtime_1.jsx)("h2", { className: classNames?.successTitle, style: styles?.successTitle, children: copy.successTitle }), (0, jsx_runtime_1.jsx)("p", { className: classNames?.successDescription, style: styles?.successDescription, children: copy.successDescription }), (0, jsx_runtime_1.jsxs)("div", { className: ['cw-feedback-success__coming-soon', classNames?.successComingSoon].filter(Boolean).join(' '), style: styles?.successComingSoon, children: [(0, jsx_runtime_1.jsxs)("svg", { className: "cw-feedback-success__leaderboard-icon", viewBox: "0 0 64 64", fill: "none", "aria-hidden": "true", children: [(0, jsx_runtime_1.jsx)("path", { d: "M7 48h50M11 48V33h12v15m8 0V23h12v25m-16 0V38h-4v10m20 0V36h10v12", stroke: "currentColor", strokeWidth: "3", strokeLinecap: "round", strokeLinejoin: "round" }), (0, jsx_runtime_1.jsx)("path", { d: "m37 7 1.8 4.2 4.5.4-3.4 3 1 4.4-3.9-2.3-3.9 2.3 1-4.4-3.4-3 4.5-.4L37 7Z", fill: "currentColor" })] }), (0, jsx_runtime_1.jsx)("span", { className: "cw-feedback-success__leaderboard-title", children: "Leaderboard" }), (0, jsx_runtime_1.jsx)("span", { className: "cw-feedback-success__leaderboard-status", children: copy.successComingSoon })] }), slots?.success] })), submitError && flow.step !== 'success' && ((0, jsx_runtime_1.jsx)("p", { role: "alert", className: ['cw-feedback-submit-error', classNames?.submitError].filter(Boolean).join(' '), style: styles?.submitError, children: copy.submitErrorLabel }))] }), !isReview && flow.step === 'gap' && canAdvanceGap && ((0, jsx_runtime_1.jsx)("div", { className: ['cw-feedback-dialog__action', classNames?.actionBar].filter(Boolean).join(' '), style: styles?.actionBar, children: (0, jsx_runtime_1.jsx)("button", { className: ['cw-feedback-primary', classNames?.primaryButton].filter(Boolean).join(' '), style: styles?.primaryButton, type: "button", onClick: isGapFirstFlow
632
+ ? () => { void persistAndAdvance('useful', () => flow.setStep('useful')); }
633
+ : submitFeedback, children: isGapFirstFlow ? copy.nextLabel : copy.doneLabel }) })), !isReview && flow.step === 'useful' && ((0, jsx_runtime_1.jsxs)("div", { className: ['cw-feedback-dialog__action', classNames?.actionBar].filter(Boolean).join(' '), style: styles?.actionBar, children: [showLowRatingTime &&
634
+ (!flow.usefulDetailShown || Boolean(flow.mostUseful.trim())) && ((0, jsx_runtime_1.jsx)("button", { className: ['cw-feedback-hint', 'cw-feedback-hint-action', classNames?.hint, classNames?.enterHintButton]
635
+ .filter(Boolean)
636
+ .join(' '), style: {
637
+ ...styles?.hint,
638
+ ...styles?.enterHintButton,
639
+ }, type: "button", onClick: continueUseful, children: copy.enterHint })), showLowRatingTime && flow.timeSavedMinutes !== undefined &&
640
+ Boolean(flow.mostUseful.trim()) &&
641
+ (wordCount(flow.mostUseful) >= copy.usefulMinimumWords || flow.usefulDetailShown) && ((0, jsx_runtime_1.jsx)("button", { className: ['cw-feedback-primary', classNames?.primaryButton].filter(Boolean).join(' '), style: styles?.primaryButton, type: "button", onClick: submitFeedback, children: copy.doneLabel }))] })), isReview && ((0, jsx_runtime_1.jsxs)("div", { className: ['cw-feedback-review-nav', classNames?.reviewNav].filter(Boolean).join(' '), style: styles?.reviewNav, children: [(0, jsx_runtime_1.jsx)("button", { className: ['cw-feedback-review-nav__button', classNames?.reviewNavButton].filter(Boolean).join(' '), style: styles?.reviewNavButton, type: "button", onClick: previousReviewPage, disabled: reviewPageIndex === 0, "aria-label": copy.backLabel, children: (0, jsx_runtime_1.jsx)(lucide_react_1.ArrowLeft, { size: 18 }) }), (0, jsx_runtime_1.jsxs)("button", { className: [
642
+ 'cw-feedback-review-nav__button',
643
+ 'cw-feedback-review-nav__button--next',
644
+ classNames?.reviewNavButton,
645
+ classNames?.reviewNavNextButton,
646
+ ]
647
+ .filter(Boolean)
648
+ .join(' '), style: {
649
+ ...styles?.reviewNavButton,
650
+ ...styles?.reviewNavNextButton,
651
+ }, type: "button", onClick: nextReviewPage, disabled: !canContinueReview, "aria-label": reviewPageIndex === reviewPages.length - 1 ? copy.saveChangesLabel : copy.nextLabel, children: [reviewPageIndex === reviewPages.length - 1 && (0, jsx_runtime_1.jsx)("span", { children: copy.saveChangesLabel }), (0, jsx_runtime_1.jsx)(lucide_react_1.ArrowRight, { size: 18 })] })] })), busy && ((0, jsx_runtime_1.jsx)("div", { ref: savingOverlayRef, className: "cw-feedback-saving-overlay", role: "status", "aria-live": "polite", tabIndex: -1, onKeyDown: (event) => { event.preventDefault(); event.stopPropagation(); }, children: (0, jsx_runtime_1.jsxs)("div", { className: "cw-feedback-saving-overlay__card", children: [(0, jsx_runtime_1.jsx)("span", { className: "cw-feedback-saving__spinner", "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("span", { children: copy.savingLabel })] }) }))] }));
652
+ const previewDialog = screenshotPreview && ((0, jsx_runtime_1.jsx)("div", { className: "cw-feedback-image-preview-backdrop", onMouseDown: (event) => {
653
+ if (event.target === event.currentTarget)
654
+ setScreenshotPreview(null);
655
+ }, children: (0, jsx_runtime_1.jsxs)("div", { ref: screenshotPreviewRef, className: "cw-feedback-image-preview-dialog", role: "dialog", "aria-modal": "true", "aria-label": `Screenshot preview: ${screenshotPreview.name}`, tabIndex: -1, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "cw-feedback-image-preview-close", onClick: () => setScreenshotPreview(null), "aria-label": "Close screenshot preview", children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 22 }) }), (0, jsx_runtime_1.jsx)("img", { src: screenshotPreview.url, alt: screenshotPreview.name })] }) }));
656
+ return typeof document !== 'undefined'
657
+ ? (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, react_dom_1.createPortal)(backdrop, document.body), (0, react_dom_1.createPortal)(dialog, document.body), previewDialog && (0, react_dom_1.createPortal)(previewDialog, document.body)] })
658
+ : (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [backdrop, dialog, previewDialog] });
659
+ }