react-visual-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/LICENSE +21 -0
- package/README.md +321 -0
- package/dist/index.css +1 -0
- package/dist/index.esm.css +1 -0
- package/dist/index.esm.js +4 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Murali
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# Murali Feedback Widget React
|
|
2
|
+
|
|
3
|
+
A powerful, visual feedback collection tool for React applications. Users can select any element on your page, and the widget automatically captures a screenshot and context information.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🎯 Visual element selection with hover highlighting
|
|
8
|
+
- 📸 Automatic screenshot capture of selected elements
|
|
9
|
+
- 📝 Feedback form with rich context
|
|
10
|
+
- ⚡ Lightweight and performant
|
|
11
|
+
- 🎨 Customizable styling
|
|
12
|
+
- ⌨️ Keyboard shortcuts (Ctrl+Q to activate, Esc to cancel)
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install murali-feedback-widget-react
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**Important:** Import the CSS file in your application:
|
|
21
|
+
|
|
22
|
+
```jsx
|
|
23
|
+
import 'murali-feedback-widget-react/dist/index.css';
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
### 1. Wrap your app with FeedbackProvider
|
|
29
|
+
|
|
30
|
+
```jsx
|
|
31
|
+
import React from 'react';
|
|
32
|
+
import { FeedbackProvider } from 'murali-feedback-widget-react';
|
|
33
|
+
import 'murali-feedback-widget-react/dist/index.css';
|
|
34
|
+
|
|
35
|
+
function App() {
|
|
36
|
+
const handleFeedbackSubmit = async (feedbackData) => {
|
|
37
|
+
console.log('Feedback received:', feedbackData);
|
|
38
|
+
// Send to your backend
|
|
39
|
+
await fetch('/api/feedback', {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: { 'Content-Type': 'application/json' },
|
|
42
|
+
body: JSON.stringify(feedbackData)
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<FeedbackProvider onSubmit={handleFeedbackSubmit}>
|
|
48
|
+
<YourApp />
|
|
49
|
+
</FeedbackProvider>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default App;
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 2. Add a feedback trigger button (optional)
|
|
57
|
+
|
|
58
|
+
```jsx
|
|
59
|
+
import { useFeedback } from 'murali-feedback-widget-react';
|
|
60
|
+
|
|
61
|
+
function FeedbackButton() {
|
|
62
|
+
const { setIsActive } = useFeedback();
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<button onClick={() => setIsActive(true)}>
|
|
66
|
+
Report Issue
|
|
67
|
+
</button>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Usage
|
|
73
|
+
|
|
74
|
+
### Keyboard Shortcut
|
|
75
|
+
Press **Ctrl+Q** to activate the feedback widget. Press **Esc** to deactivate.
|
|
76
|
+
|
|
77
|
+
### Programmatic Activation (Uncontrolled Mode)
|
|
78
|
+
Use the `useFeedback` hook to control the widget programmatically:
|
|
79
|
+
|
|
80
|
+
```jsx
|
|
81
|
+
import { useFeedback } from 'murali-feedback-widget-react';
|
|
82
|
+
|
|
83
|
+
function MyComponent() {
|
|
84
|
+
const { isActive, setIsActive } = useFeedback();
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<button onClick={() => setIsActive(!isActive)}>
|
|
88
|
+
{isActive ? 'Cancel Feedback' : 'Give Feedback'}
|
|
89
|
+
</button>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Controlled Mode
|
|
95
|
+
You can control the widget's active state from the parent component:
|
|
96
|
+
|
|
97
|
+
```jsx
|
|
98
|
+
import React, { useState } from 'react';
|
|
99
|
+
import { FeedbackProvider } from 'murali-feedback-widget-react';
|
|
100
|
+
|
|
101
|
+
function App() {
|
|
102
|
+
const [isFeedbackActive, setIsFeedbackActive] = useState(false);
|
|
103
|
+
|
|
104
|
+
const handleFeedbackSubmit = async (feedbackData) => {
|
|
105
|
+
console.log('Feedback:', feedbackData);
|
|
106
|
+
// Submit to your backend
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div>
|
|
111
|
+
<button onClick={() => setIsFeedbackActive(!isFeedbackActive)}>
|
|
112
|
+
{isFeedbackActive ? 'Cancel' : 'Report Bug'}
|
|
113
|
+
</button>
|
|
114
|
+
|
|
115
|
+
<FeedbackProvider
|
|
116
|
+
onSubmit={handleFeedbackSubmit}
|
|
117
|
+
isActive={isFeedbackActive}
|
|
118
|
+
onActiveChange={setIsFeedbackActive}
|
|
119
|
+
>
|
|
120
|
+
<YourApp />
|
|
121
|
+
</FeedbackProvider>
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## API Reference
|
|
128
|
+
|
|
129
|
+
### FeedbackProvider
|
|
130
|
+
|
|
131
|
+
The main provider component that wraps your application.
|
|
132
|
+
|
|
133
|
+
**Props:**
|
|
134
|
+
- `onSubmit` (required): `(feedbackData) => Promise<void>` - Callback function when feedback is submitted
|
|
135
|
+
- `children`: React nodes
|
|
136
|
+
- `isActive` (optional): `boolean` - Control the widget active state from parent (controlled mode)
|
|
137
|
+
- `onActiveChange` (optional): `(active: boolean) => void` - Callback when active state changes (used with controlled mode)
|
|
138
|
+
|
|
139
|
+
**Feedback Data Structure:**
|
|
140
|
+
```javascript
|
|
141
|
+
{
|
|
142
|
+
feedback: "User's feedback text",
|
|
143
|
+
elementInfo: {
|
|
144
|
+
tagName: "div",
|
|
145
|
+
id: "element-id",
|
|
146
|
+
className: "element-classes",
|
|
147
|
+
xpath: "//div[@id='element-id']",
|
|
148
|
+
innerText: "Element text content",
|
|
149
|
+
attributes: { /* element attributes */ }
|
|
150
|
+
},
|
|
151
|
+
screenshot: "data:image/png;base64,...", // Base64 encoded screenshot
|
|
152
|
+
url: "https://yourapp.com/current-page",
|
|
153
|
+
userAgent: "Mozilla/5.0...",
|
|
154
|
+
timestamp: "2025-10-22T10:30:00.000Z"
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### useFeedback
|
|
159
|
+
|
|
160
|
+
Hook to access feedback widget state and controls.
|
|
161
|
+
|
|
162
|
+
**Returns:**
|
|
163
|
+
- `isActive`: boolean - Whether the widget is currently active
|
|
164
|
+
- `setIsActive`: (active: boolean) => void - Function to activate/deactivate the widget
|
|
165
|
+
|
|
166
|
+
## Styling
|
|
167
|
+
|
|
168
|
+
The widget comes with default styles, but you can customize them by targeting these CSS classes:
|
|
169
|
+
|
|
170
|
+
```css
|
|
171
|
+
.feedback-overlay { /* Background overlay when active */ }
|
|
172
|
+
.feedback-highlight { /* Element highlight border */ }
|
|
173
|
+
.feedback-tooltip { /* Element info tooltip */ }
|
|
174
|
+
.feedback-modal { /* Feedback form modal */ }
|
|
175
|
+
.feedback-backdrop { /* Modal backdrop */ }
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Example Implementation
|
|
179
|
+
|
|
180
|
+
```jsx
|
|
181
|
+
import React from 'react';
|
|
182
|
+
import { FeedbackProvider, useFeedback } from 'murali-feedback-widget-react';
|
|
183
|
+
|
|
184
|
+
function FeedbackButton() {
|
|
185
|
+
const { isActive, setIsActive } = useFeedback();
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<button
|
|
189
|
+
onClick={() => setIsActive(true)}
|
|
190
|
+
style={{
|
|
191
|
+
position: 'fixed',
|
|
192
|
+
bottom: '20px',
|
|
193
|
+
right: '20px',
|
|
194
|
+
padding: '10px 20px',
|
|
195
|
+
background: '#007bff',
|
|
196
|
+
color: 'white',
|
|
197
|
+
border: 'none',
|
|
198
|
+
borderRadius: '5px',
|
|
199
|
+
cursor: 'pointer'
|
|
200
|
+
}}
|
|
201
|
+
>
|
|
202
|
+
{isActive ? 'Select Element...' : 'Report Bug'}
|
|
203
|
+
</button>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function App() {
|
|
208
|
+
const handleFeedbackSubmit = async (feedbackData) => {
|
|
209
|
+
try {
|
|
210
|
+
const response = await fetch('https://your-api.com/feedback', {
|
|
211
|
+
method: 'POST',
|
|
212
|
+
headers: { 'Content-Type': 'application/json' },
|
|
213
|
+
body: JSON.stringify(feedbackData)
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
if (response.ok) {
|
|
217
|
+
alert('Thank you for your feedback!');
|
|
218
|
+
}
|
|
219
|
+
} catch (error) {
|
|
220
|
+
console.error('Failed to submit feedback:', error);
|
|
221
|
+
alert('Failed to submit feedback. Please try again.');
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
<FeedbackProvider onSubmit={handleFeedbackSubmit}>
|
|
227
|
+
<div>
|
|
228
|
+
<h1>My Application</h1>
|
|
229
|
+
<p>Press Ctrl+Q or click the button to report issues</p>
|
|
230
|
+
<FeedbackButton />
|
|
231
|
+
</div>
|
|
232
|
+
</FeedbackProvider>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export default App;
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## How It Works
|
|
240
|
+
|
|
241
|
+
1. User activates the widget (Ctrl+Q or button click)
|
|
242
|
+
2. User hovers over elements to see them highlighted
|
|
243
|
+
3. User clicks on the problematic element
|
|
244
|
+
4. Widget captures a screenshot of the selected element
|
|
245
|
+
5. Feedback form appears with element context pre-filled
|
|
246
|
+
6. User enters their feedback and submits
|
|
247
|
+
7. Your `onSubmit` handler receives all the data
|
|
248
|
+
|
|
249
|
+
## Browser Support
|
|
250
|
+
|
|
251
|
+
- Chrome/Edge: ✅
|
|
252
|
+
- Firefox: ✅
|
|
253
|
+
- Safari: ✅
|
|
254
|
+
- Opera: ✅
|
|
255
|
+
|
|
256
|
+
Requires `html2canvas` for screenshot functionality.
|
|
257
|
+
|
|
258
|
+
## Dependencies
|
|
259
|
+
|
|
260
|
+
- React ^16.8.0 || ^17.0.0 || ^18.0.0
|
|
261
|
+
- react-dom ^16.8.0 || ^17.0.0 || ^18.0.0
|
|
262
|
+
- html2canvas ^1.4.1
|
|
263
|
+
- lucide-react ^0.263.1
|
|
264
|
+
|
|
265
|
+
## Local Development & Testing
|
|
266
|
+
|
|
267
|
+
Want to test the widget locally? We've included a complete example app!
|
|
268
|
+
|
|
269
|
+
### Quick Start
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
# 1. Clone the repository
|
|
273
|
+
git clone https://github.com/Murali1889/react-feedback-widget.git
|
|
274
|
+
cd react-feedback-widget
|
|
275
|
+
|
|
276
|
+
# 2. Install dependencies
|
|
277
|
+
npm install
|
|
278
|
+
|
|
279
|
+
# 3. Build the widget
|
|
280
|
+
npm run build
|
|
281
|
+
|
|
282
|
+
# 4. Run the example app
|
|
283
|
+
cd example
|
|
284
|
+
npm install
|
|
285
|
+
npm run dev
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
The example app will open at `http://localhost:3000` with a fully working demo!
|
|
289
|
+
|
|
290
|
+
### What's Included
|
|
291
|
+
|
|
292
|
+
- ✅ Complete working example with UI
|
|
293
|
+
- ✅ Both controlled and uncontrolled mode examples
|
|
294
|
+
- ✅ Interactive test elements (buttons, forms, images)
|
|
295
|
+
- ✅ Console logging to see feedback data
|
|
296
|
+
- ✅ Hot reload for fast development
|
|
297
|
+
|
|
298
|
+
### Making Changes
|
|
299
|
+
|
|
300
|
+
1. Edit source files in `src/`
|
|
301
|
+
2. Run `npm run build` in the root directory
|
|
302
|
+
3. Refresh the example app browser - changes will be reflected!
|
|
303
|
+
|
|
304
|
+
See `example/README.md` for more details.
|
|
305
|
+
|
|
306
|
+
## License
|
|
307
|
+
|
|
308
|
+
MIT © Murali
|
|
309
|
+
|
|
310
|
+
## Contributing
|
|
311
|
+
|
|
312
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
313
|
+
|
|
314
|
+
## Issues
|
|
315
|
+
|
|
316
|
+
If you encounter any issues, please report them at:
|
|
317
|
+
https://github.com/Murali1889/react-feedback-widget/issues
|
|
318
|
+
|
|
319
|
+
## Author
|
|
320
|
+
|
|
321
|
+
Murali
|
package/dist/index.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.feedback-overlay{background:rgba(0,0,0,.05);bottom:0;cursor:crosshair;left:0;pointer-events:none;position:fixed;right:0;top:0;z-index:999998}.feedback-highlight{background:rgba(59,130,246,.1);border:2px solid #3b82f6;box-shadow:0 0 0 4px rgba(59,130,246,.2);pointer-events:none;position:absolute;transition:all .1s ease;z-index:999999}.feedback-tooltip{background:#1f2937;border-radius:6px;box-shadow:0 4px 6px rgba(0,0,0,.3);color:#fff;font-family:Courier New,monospace;font-size:12px;padding:6px 12px;pointer-events:none;position:fixed;white-space:nowrap;z-index:1000000}.feedback-backdrop{animation:fadeIn .2s ease;background:rgba(0,0,0,.5);bottom:0;left:0;position:fixed;right:0;top:0;z-index:1000001}.feedback-modal{animation:slideUp .3s ease;left:50%;max-height:90vh;max-width:1200px;position:fixed;top:50%;transform:translate(-50%,-50%);width:95%;z-index:1000002}.feedback-modal-content{background:#fff;border-radius:12px;box-shadow:0 20px 25px -5px rgba(0,0,0,.2),0 10px 10px -5px rgba(0,0,0,.1);display:flex;flex-direction:column;max-height:90vh;padding:24px}.feedback-header{align-items:center;display:flex;justify-content:space-between;margin-bottom:20px}.feedback-title{align-items:center;color:#111827;display:flex;font-size:18px;font-weight:600;gap:8px;margin:0}.feedback-close{align-items:center;background:none;border:none;border-radius:4px;color:#6b7280;cursor:pointer;display:flex;padding:4px;transition:background .2s}.feedback-close:hover{background:#f3f4f6;color:#111827}.feedback-body{align-items:center;display:flex;flex:1;gap:20px;justify-content:center;overflow:auto}.feedback-screenshot-container{display:flex;flex:1.2;flex-direction:column;min-width:0}.feedback-screenshot{background:#fff;border:2px solid #e5e7eb;border-radius:8px;box-sizing:border-box;max-height:500px;overflow:auto;padding:12px;width:100%}.feedback-screenshot-img{border-radius:4px;display:block;height:auto;width:100%}.feedback-form-container{min-width:300px}.feedback-form,.feedback-form-container{display:flex;flex:1;flex-direction:column}.feedback-form{margin-bottom:16px}@media (max-width:768px){.feedback-modal{max-width:95%;width:95%}.feedback-body{flex-direction:column}.feedback-screenshot-container{flex:none;max-height:300px}.feedback-screenshot{max-height:300px}.feedback-form-container{min-width:0}.feedback-modal-content{padding:16px}}.feedback-label{color:#374151;display:block;font-size:14px;font-weight:500;margin-bottom:8px}.feedback-textarea{border:1px solid #d1d5db;border-radius:8px;box-sizing:border-box;flex:1;font-family:inherit;font-size:14px;min-height:200px;padding:12px;resize:vertical;transition:border-color .2s,box-shadow .2s;width:100%}.feedback-textarea:focus{border-color:#3b82f6;box-shadow:0 0 0 3px rgba(59,130,246,.1);outline:none}.feedback-textarea:disabled{background:#f9fafb;color:#6b7280;cursor:not-allowed}.feedback-actions{display:flex;gap:12px;justify-content:flex-end}.feedback-btn{align-items:center;border:none;border-radius:8px;cursor:pointer;display:flex;font-size:14px;font-weight:500;gap:6px;padding:10px 20px;transition:all .2s}.feedback-cancel{background:#f3f4f6;color:#374151}.feedback-cancel:hover:not(:disabled){background:#e5e7eb}.feedback-submit{background:#3b82f6;color:#fff}.feedback-submit:hover:not(:disabled){background:#2563eb;box-shadow:0 4px 6px rgba(59,130,246,.3);transform:translateY(-1px)}.feedback-submit:disabled{background:#9ca3af;cursor:not-allowed;transform:none}.feedback-loading{animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(1turn)}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes slideUp{0%{opacity:0;transform:translate(-50%,-40%)}to{opacity:1;transform:translate(-50%,-50%)}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.feedback-overlay{background:rgba(0,0,0,.05);bottom:0;cursor:crosshair;left:0;pointer-events:none;position:fixed;right:0;top:0;z-index:999998}.feedback-highlight{background:rgba(59,130,246,.1);border:2px solid #3b82f6;box-shadow:0 0 0 4px rgba(59,130,246,.2);pointer-events:none;position:absolute;transition:all .1s ease;z-index:999999}.feedback-tooltip{background:#1f2937;border-radius:6px;box-shadow:0 4px 6px rgba(0,0,0,.3);color:#fff;font-family:Courier New,monospace;font-size:12px;padding:6px 12px;pointer-events:none;position:fixed;white-space:nowrap;z-index:1000000}.feedback-backdrop{animation:fadeIn .2s ease;background:rgba(0,0,0,.5);bottom:0;left:0;position:fixed;right:0;top:0;z-index:1000001}.feedback-modal{animation:slideUp .3s ease;left:50%;max-height:90vh;max-width:1200px;position:fixed;top:50%;transform:translate(-50%,-50%);width:95%;z-index:1000002}.feedback-modal-content{background:#fff;border-radius:12px;box-shadow:0 20px 25px -5px rgba(0,0,0,.2),0 10px 10px -5px rgba(0,0,0,.1);display:flex;flex-direction:column;max-height:90vh;padding:24px}.feedback-header{align-items:center;display:flex;justify-content:space-between;margin-bottom:20px}.feedback-title{align-items:center;color:#111827;display:flex;font-size:18px;font-weight:600;gap:8px;margin:0}.feedback-close{align-items:center;background:none;border:none;border-radius:4px;color:#6b7280;cursor:pointer;display:flex;padding:4px;transition:background .2s}.feedback-close:hover{background:#f3f4f6;color:#111827}.feedback-body{align-items:center;display:flex;flex:1;gap:20px;justify-content:center;overflow:auto}.feedback-screenshot-container{display:flex;flex:1.2;flex-direction:column;min-width:0}.feedback-screenshot{background:#fff;border:2px solid #e5e7eb;border-radius:8px;box-sizing:border-box;max-height:500px;overflow:auto;padding:12px;width:100%}.feedback-screenshot-img{border-radius:4px;display:block;height:auto;width:100%}.feedback-form-container{min-width:300px}.feedback-form,.feedback-form-container{display:flex;flex:1;flex-direction:column}.feedback-form{margin-bottom:16px}@media (max-width:768px){.feedback-modal{max-width:95%;width:95%}.feedback-body{flex-direction:column}.feedback-screenshot-container{flex:none;max-height:300px}.feedback-screenshot{max-height:300px}.feedback-form-container{min-width:0}.feedback-modal-content{padding:16px}}.feedback-label{color:#374151;display:block;font-size:14px;font-weight:500;margin-bottom:8px}.feedback-textarea{border:1px solid #d1d5db;border-radius:8px;box-sizing:border-box;flex:1;font-family:inherit;font-size:14px;min-height:200px;padding:12px;resize:vertical;transition:border-color .2s,box-shadow .2s;width:100%}.feedback-textarea:focus{border-color:#3b82f6;box-shadow:0 0 0 3px rgba(59,130,246,.1);outline:none}.feedback-textarea:disabled{background:#f9fafb;color:#6b7280;cursor:not-allowed}.feedback-actions{display:flex;gap:12px;justify-content:flex-end}.feedback-btn{align-items:center;border:none;border-radius:8px;cursor:pointer;display:flex;font-size:14px;font-weight:500;gap:6px;padding:10px 20px;transition:all .2s}.feedback-cancel{background:#f3f4f6;color:#374151}.feedback-cancel:hover:not(:disabled){background:#e5e7eb}.feedback-submit{background:#3b82f6;color:#fff}.feedback-submit:hover:not(:disabled){background:#2563eb;box-shadow:0 4px 6px rgba(59,130,246,.3);transform:translateY(-1px)}.feedback-submit:disabled{background:#9ca3af;cursor:not-allowed;transform:none}.feedback-loading{animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(1turn)}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes slideUp{0%{opacity:0;transform:translate(-50%,-40%)}to{opacity:1;transform:translate(-50%,-50%)}}
|