@tapni/auth 1.0.5 → 1.0.6-3.dev

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +2 -0
  2. package/dist/.vite/manifest.json +16 -43
  3. package/dist/.well-known/assetlinks.json +10 -12
  4. package/dist/.well-known/microsoft-identity-association.json +5 -5
  5. package/dist/{Apps-DMds3Dv-.js → Apps-XNA4_3B4.js} +34 -34
  6. package/dist/Billing-Br0-fHed.js +256 -0
  7. package/dist/CustomApp-CrlbYDOm.js +83 -0
  8. package/dist/QR-Bvqb60-E.js +41 -0
  9. package/dist/TapniAuth.es.js +1 -1
  10. package/dist/TapniAuth.umd.js +49 -23
  11. package/dist/{install-L-cxSovH.js → install-7FOVy8Ol.js} +6267 -4743
  12. package/dist/site.webmanifest +11 -1
  13. package/dist/style.css +1 -1
  14. package/dist/{web-IFGkBi0t.js → web-UrTMimK1.js} +2 -2
  15. package/package.json +65 -55
  16. package/src/.prettierrc.json +16 -0
  17. package/src/App.vue +326 -269
  18. package/src/eslint.config.js +15 -0
  19. package/src/index.js +4 -0
  20. package/src/install.js +9 -10
  21. package/src/main.js +54 -57
  22. package/src/mixins/apple.mixin.js +56 -54
  23. package/src/mixins/auth.mixin.js +3 -2
  24. package/src/mixins/global.mixin.js +3 -3
  25. package/src/mixins/google.mixin.js +53 -54
  26. package/src/mixins/microsoft.mixin.js +2 -5
  27. package/src/mixins/okta.mixin.js +1 -1
  28. package/src/mixins/qr-auth.mixin.js +111 -107
  29. package/src/mixins/saml.mixin.js +82 -45
  30. package/src/router/index.js +6 -6
  31. package/src/routes.js +1 -1
  32. package/src/services/Api.js +56 -58
  33. package/src/services/AuthService.js +7 -9
  34. package/src/services/CompanyService.js +10 -10
  35. package/src/services/DeviceService.js +3 -3
  36. package/src/services/UserService.js +48 -45
  37. package/src/services/UtilService.js +317 -225
  38. package/src/store/auth.js +485 -549
  39. package/src/store/constants.js +2 -2
  40. package/src/store/event-bus.js +22 -22
  41. package/src/store/locales/cn.js +476 -458
  42. package/src/store/locales/de.js +478 -517
  43. package/src/store/locales/en.js +454 -513
  44. package/src/store/locales/es.js +477 -524
  45. package/src/store/locales/fr.js +477 -516
  46. package/src/store/locales/it.js +477 -514
  47. package/src/store/locales/ja.js +488 -0
  48. package/src/store/locales/kr.js +477 -491
  49. package/src/store/locales/lang.js +51 -43
  50. package/src/store/locales/pt.js +488 -0
  51. package/src/store/locales/sr.js +477 -492
  52. package/src/store/locales/tr.js +477 -487
  53. package/src/store/store.js +6 -6
  54. package/src/views/Account.vue +36 -8
  55. package/src/views/Billing.vue +464 -34
  56. package/src/views/Callback.vue +36 -33
  57. package/src/views/General.vue +151 -185
  58. package/src/views/Login.vue +2 -25
  59. package/src/views/Register.vue +2 -12
  60. package/src/views/Reset.vue +132 -135
  61. package/src/views/Security.vue +13 -7
  62. package/src/views/Verify.vue +153 -151
  63. package/src/views/Welcome.vue +85 -71
  64. package/dist/Account-Cuz87g_8.js +0 -153
  65. package/dist/Billing-BXlQEuNy.js +0 -113
  66. package/dist/CustomApp-CLCMXmMO.js +0 -83
  67. package/dist/General-dW73bMoR.js +0 -479
  68. package/dist/QR-D6ZGcPM0.js +0 -41
  69. package/dist/index.css +0 -193
  70. package/dist/web-AXRKjAOB.js +0 -92
  71. package/src/components/DELETE_Language.vue +0 -168
  72. package/src/components/DELETE_LinkIcon.vue +0 -288
  73. package/src/components/DELETE_ModalOverlay.vue +0 -68
  74. package/src/components/DELETE_OTP.vue +0 -105
  75. package/src/components/DELETE_SSO.vue +0 -120
  76. package/src/components/DELETE_SSOPick.vue +0 -166
  77. package/src/mixins/DELETE_mfa-auth.mixin.js +0 -53
  78. package/src/mixins/facebook.mixin.js +0 -78
@@ -1,8 +1,8 @@
1
- import { createStore } from 'vuex'
2
- import auth from './auth.js'
1
+ import { createStore } from 'vuex';
2
+ import auth from './auth.js';
3
3
 
4
4
  export default createStore({
5
- modules: {
6
- auth
7
- },
8
- })
5
+ modules: {
6
+ auth
7
+ }
8
+ });
@@ -2,7 +2,7 @@
2
2
  <div class="page-login content-boxed content-boxed-padding center-text" style="margin-top: -1px; overflow: hidden; border: solid 0px #ffffff;">
3
3
  <br>
4
4
  <img v-if="false" :src="account.photo" class="user-photo margin-center" style="max-width: 110px;" alt="">
5
- <h1 class="bold full-top no-bottom center-text">{{ssoLang[appLanguage].welcome_account}}</h1>
5
+ <h1 class="bold full-top no-bottom center-text text-[24px]">{{ssoLang[appLanguage].welcome_account}}</h1>
6
6
  <p class="full-bottom half-top center-text font-16">{{account.email}}</p>
7
7
 
8
8
  <div v-if="display !== 'npm' && false" class="full-top full-bottom">
@@ -79,7 +79,19 @@
79
79
  </div>
80
80
  </div>
81
81
 
82
- <router-link to="/general" class="settingRow">
82
+ <div v-if="isModal" class="settingRow" @click="$emit('update:viewProp', 'AuthGeneral')">
83
+ <div class="firstRow">
84
+ <img
85
+ :src="getIcon('user-black.svg')"
86
+ class="withBackground"
87
+ />
88
+ <h4>{{ssoLang[appLanguage].general }}</h4>
89
+
90
+ <img :src="getIcon('arrow-gray-right.svg')" />
91
+ </div>
92
+ </div>
93
+
94
+ <router-link v-else to="/general" class="settingRow">
83
95
  <div class="firstRow">
84
96
  <img
85
97
  :src="getIcon('user-black.svg')"
@@ -91,7 +103,19 @@
91
103
  </div>
92
104
  </router-link>
93
105
 
94
- <router-link to="/security" class="settingRow">
106
+ <div v-if="isModal" class="settingRow" @click="$emit('update:viewProp', 'AuthSecurity')">
107
+ <div class="firstRow">
108
+ <img
109
+ :src="getIcon('shield-black.svg')"
110
+ class="withBackground"
111
+ />
112
+ <h4>{{ssoLang[appLanguage].security }}</h4>
113
+
114
+ <img :src="getIcon('arrow-gray-right.svg')" />
115
+ </div>
116
+ </div>
117
+
118
+ <router-link v-else to="/security" class="settingRow">
95
119
  <div class="firstRow">
96
120
  <img
97
121
  :src="getIcon('shield-black.svg')"
@@ -103,7 +127,7 @@
103
127
  </div>
104
128
  </router-link>
105
129
 
106
- <router-link to="/apps" class="settingRow">
130
+ <router-link v-if="false" to="/apps" class="settingRow">
107
131
  <div class="firstRow">
108
132
  <img
109
133
  :src="getIcon('apps-black.svg')"
@@ -128,7 +152,7 @@
128
152
  </router-link>
129
153
 
130
154
 
131
- <div class="settingRow" @click="logoutAccount">
155
+ <div v-if="!isModal" class="settingRow" @click="logoutAccount">
132
156
  <div class="firstRow">
133
157
  <img
134
158
  :src="getIcon('logout-red.svg')"
@@ -159,6 +183,12 @@ import { EventBus } from "../store/event-bus.js";
159
183
  import CONSTANTS from "../store/constants.js";
160
184
  export default {
161
185
  mixins: [AuthMixin],
186
+ props: {
187
+ isModal: {
188
+ type: Boolean,
189
+ default: false
190
+ }
191
+ },
162
192
  data () {
163
193
  return {
164
194
  expanded: false,
@@ -174,17 +204,15 @@ export default {
174
204
  if (!this.isLoggedIn) {
175
205
  return this.$router.push('/');
176
206
  }
177
- this.getAccountSettings();
207
+ await this.getAccountSettings();
178
208
  },
179
209
  methods: {
180
210
  continueTo (realm) {
181
- console.log('continue to ' + realm);
182
211
  },
183
212
  addAccount () {
184
213
  this.$router.push('/login');
185
214
  },
186
215
  switchAccount (username) {
187
- console.log('switch', username);
188
216
  },
189
217
  toggleLanguageModal () {
190
218
  EventBus.$emit('toggleSSOLanguageModal')
@@ -27,23 +27,98 @@
27
27
  <p class="center-text">{{ssoLang[appLanguage].billing_p }}</p>
28
28
 
29
29
  <div class="full-top">
30
- <h3 v-if="false" class="full-top bold small-bottom">{{ssoLang[appLanguage].apps_my}}</h3>
30
+ <!-- No subscriptions message -->
31
+ <div v-if="subscriptions.length === 0" class="no-subscriptions center-text full-top">
32
+ <p class="gray-text">There are no active subscriptions at this point.</p>
33
+ </div>
31
34
 
32
- {{account.billing}}
35
+ <!-- Subscriptions list -->
36
+ <div v-else>
37
+ <div v-for="sub in subscriptions" :key="sub.subscriptionId" class="subscription-card half-bottom">
38
+ <div class="subscription-header">
39
+ <div class="subscription-info">
40
+ <h3 class="subscription-name">{{ sub.subscriptionName }}</h3>
41
+ <span class="subscription-status" :class="getStatusClass(sub.status)">
42
+ {{ getStatusText(sub.status) }}
43
+ </span>
44
+ </div>
45
+ </div>
33
46
 
34
- <div v-for="sub in subscriptions" :key="sub.id" class="settingRow half-bottom">
35
- <div class="firstRow">
36
- <img
37
- :src="getIcon(app.t_apps_icon)"
38
- class="withBackground"
39
- />
40
- <h4>{{ sub.t_subscriptions_name }}</h4>
47
+ <div class="subscription-details">
48
+ <div class="detail-row">
49
+ <span class="detail-label">Amount:</span>
50
+ <span class="detail-value">{{ formatCurrency(sub.amount, sub.currency) }}</span>
51
+ </div>
52
+ <div class="detail-row">
53
+ <span class="detail-label">Billing:</span>
54
+ <span class="detail-value">{{ formatInterval(sub.interval) }}</span>
55
+ </div>
56
+ <div class="detail-row">
57
+ <span class="detail-label">Licenses:</span>
58
+ <span class="detail-value">{{ sub.licenses }}</span>
59
+ </div>
60
+ <div v-if="sub.isTrial" class="detail-row">
61
+ <span class="detail-label">Trial Ends:</span>
62
+ <span class="detail-value">{{ formatDate(sub.trialEnd) }}</span>
63
+ </div>
64
+ <div v-else-if="sub.endDate" class="detail-row">
65
+ <span class="detail-label">Next Billing:</span>
66
+ <span class="detail-value">{{ formatDate(sub.endDate) }}</span>
67
+ </div>
68
+ </div>
41
69
 
42
- <img :src="getIcon('arrow-gray-right.svg')" />
70
+ <div class="subscription-actions" v-if="sub.status !== 'canceled'">
71
+ <button
72
+ @click="openCancelModal(sub)"
73
+ class="cancel-button"
74
+ :disabled="loading"
75
+ >
76
+ {{ loading ? 'Processing...' : 'Cancel Subscription' }}
77
+ </button>
78
+ </div>
43
79
  </div>
44
80
  </div>
45
81
  </div>
46
82
  </div>
83
+
84
+ <!-- Cancel Confirmation Modal -->
85
+ <div v-if="showCancelModal" class="modal-overlay" @click.self="closeCancelModal">
86
+ <div class="modal-content">
87
+ <div class="modal-header">
88
+ <h3>Cancel Subscription</h3>
89
+ <button @click="closeCancelModal" class="close-button">&times;</button>
90
+ </div>
91
+
92
+ <div class="modal-body">
93
+ <p class="modal-text">
94
+ Are you sure you want to cancel your subscription to <b>{{ selectedSubscription?.subscriptionName }}</b>?
95
+ </p>
96
+
97
+ <div class="feedback-section">
98
+ <label class="feedback-label">We'd love to know why you're canceling (optional):</label>
99
+ <textarea
100
+ v-model="cancelFeedback"
101
+ class="feedback-textarea"
102
+ placeholder="Your feedback helps us improve our service..."
103
+ rows="4"
104
+ ></textarea>
105
+ </div>
106
+ </div>
107
+
108
+ <div class="modal-footer">
109
+ <button @click="closeCancelModal" class="button-secondary">
110
+ Keep Subscription
111
+ </button>
112
+ <button
113
+ @click="confirmCancel"
114
+ class="button-danger"
115
+ :disabled="loading"
116
+ >
117
+ {{ loading ? 'Canceling...' : 'Confirm Cancellation' }}
118
+ </button>
119
+ </div>
120
+ </div>
121
+ </div>
47
122
  </div>
48
123
  </template>
49
124
 
@@ -51,7 +126,7 @@
51
126
  <script>
52
127
  import AuthMixin from "../mixins/auth.mixin";
53
128
  import {EventBus} from "@/store/event-bus.js";
54
- import AuthService from "@/services/AuthService.js";
129
+ import api from "@/services/Api.js";
55
130
 
56
131
  export default {
57
132
  name: "AuthBilling",
@@ -65,38 +140,132 @@ export default {
65
140
  data () {
66
141
  return {
67
142
  loading: false,
68
- subscriptions: []
143
+ subscriptions: [],
144
+ showCancelModal: false,
145
+ selectedSubscription: null,
146
+ cancelFeedback: ''
69
147
  }
70
148
  },
71
149
  async mounted() {
72
150
  if (!this.isLoggedIn) this.$router.push('/login');
73
-
74
-
75
- const response = await AuthService.getRecords({
76
- objectId: '6dc545ea-2e2f-4720-b4fe-c5ebdd8af066',
77
- query: {
78
- where: {
79
- account: this.account.id,
80
- },
81
- fields: ['t_subscriptions_name', 't_subscriptions_desc', 't_subscriptions_type', 't_subscriptions_active', 't_subscriptions_app'],
82
- relations: {
83
- t_subscriptions_app: {
84
- fields: [
85
- "t_apps_name",
86
- "t_apps_icon",
87
- "t_apps_url"
88
- ],
89
- relations: {},
90
- }
91
- }
92
- }
93
- })
94
- this.subscriptions = response.data.records;
151
+ await this.getAccountSettings();
152
+ this.loadSubscriptions();
95
153
  },
96
154
  methods: {
97
155
  close () {
98
156
  EventBus.$emit('ssoEvent', {name: 'toggleAuthModal', data: true})
99
157
  },
158
+ loadSubscriptions() {
159
+ // Extract subscriptions from account.billing
160
+ if (this.account.billing) {
161
+ const billing = this.account.billing;
162
+ const subs = [];
163
+
164
+ // Loop through billing object to find subscription objects
165
+ for (const key in billing) {
166
+ if (key !== 'region' && key !== 'currency' && key !== 'paymentProfiles' && typeof billing[key] === 'object' && billing[key].subscriptionId) {
167
+ subs.push(billing[key]);
168
+ }
169
+ }
170
+
171
+ this.subscriptions = subs;
172
+ }
173
+ },
174
+ formatCurrency(amount, currency) {
175
+ const currencySymbols = {
176
+ 'EUR': '€',
177
+ 'USD': '$',
178
+ 'GBP': '£'
179
+ };
180
+ const symbol = currencySymbols[currency?.toUpperCase()] || currency || '';
181
+ return `${symbol}${amount?.toFixed(2) || '0.00'}`;
182
+ },
183
+ formatInterval(interval) {
184
+ return interval ? `per ${interval}` : '';
185
+ },
186
+ formatDate(timestamp) {
187
+ if (!timestamp) return '';
188
+ const date = new Date(timestamp * 1000);
189
+ return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
190
+ },
191
+ getStatusText(status) {
192
+ const statusMap = {
193
+ 'trialing': 'Trial',
194
+ 'active': 'Active',
195
+ 'past_due': 'Past Due',
196
+ 'canceled': 'Canceled',
197
+ 'unpaid': 'Unpaid'
198
+ };
199
+ return statusMap[status] || status;
200
+ },
201
+ getStatusClass(status) {
202
+ return `status-${status}`;
203
+ },
204
+ openCancelModal(subscription) {
205
+ this.selectedSubscription = subscription;
206
+ this.showCancelModal = true;
207
+ this.cancelFeedback = '';
208
+ },
209
+ closeCancelModal() {
210
+ this.showCancelModal = false;
211
+ this.selectedSubscription = null;
212
+ this.cancelFeedback = '';
213
+ },
214
+ async confirmCancel() {
215
+ if (!this.selectedSubscription) return;
216
+
217
+ // Check if this is a RevenueCat in-app purchase subscription
218
+ if (this.selectedSubscription.paymentGateway === 'revenuecat') {
219
+ // Fire event to redirect user to RevenueCat management URL
220
+ EventBus.$emit('ssoEvent', {
221
+ name: 'subscriptionCancelled',
222
+ data: {
223
+ subscriptionId: this.selectedSubscription.subscriptionId,
224
+ paymentGateway: this.selectedSubscription.paymentGateway,
225
+ feedback: this.cancelFeedback
226
+ }
227
+ });
228
+
229
+ // Close the modal
230
+ this.closeCancelModal();
231
+ return;
232
+ }
233
+
234
+ // For other payment gateways (like Stripe), proceed with API cancellation
235
+ this.loading = true;
236
+
237
+ try {
238
+ // Send cancel request to backend
239
+ const response = await api(false, 'v2').post('checkout/cancel/' + this.selectedSubscription.subscriptionId, {
240
+ feedback: this.cancelFeedback
241
+ });
242
+
243
+ if (response.data.success) {
244
+ // Show success message
245
+ EventBus.$emit('showToast', {
246
+ type: 'success',
247
+ message: 'Subscription canceled successfully'
248
+ });
249
+
250
+ // Refresh account settings to get updated billing info
251
+ await this.getAccountSettings();
252
+ this.loadSubscriptions();
253
+
254
+ // Close modal
255
+ this.closeCancelModal();
256
+ } else {
257
+ throw new Error(response.data.message || 'Failed to cancel subscription');
258
+ }
259
+ } catch (error) {
260
+ console.error('Error canceling subscription:', error);
261
+ EventBus.$emit('showToast', {
262
+ type: 'error',
263
+ message: error.response?.data?.message || 'Failed to cancel subscription. Please try again.'
264
+ });
265
+ } finally {
266
+ this.loading = false;
267
+ }
268
+ }
100
269
  }
101
270
  };
102
271
  </script>
@@ -105,4 +274,265 @@ export default {
105
274
  .withBackground {
106
275
  height: 50px;
107
276
  }
277
+
278
+ .no-subscriptions {
279
+ padding: 40px 20px;
280
+ }
281
+
282
+ .gray-text {
283
+ color: #666;
284
+ font-size: 16px;
285
+ }
286
+
287
+ .subscription-card {
288
+ background: #fff;
289
+ border: 1px solid #e0e0e0;
290
+ border-radius: 12px;
291
+ padding: 20px;
292
+ margin-bottom: 16px;
293
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
294
+ }
295
+
296
+ .subscription-header {
297
+ display: flex;
298
+ justify-content: space-between;
299
+ align-items: flex-start;
300
+ margin-bottom: 16px;
301
+ padding-bottom: 16px;
302
+ border-bottom: 1px solid #f0f0f0;
303
+ }
304
+
305
+ .subscription-info {
306
+ flex: 1;
307
+ }
308
+
309
+ .subscription-name {
310
+ font-size: 20px;
311
+ font-weight: 600;
312
+ margin: 0 0 8px 0;
313
+ color: #000;
314
+ }
315
+
316
+ .subscription-status {
317
+ display: inline-block;
318
+ padding: 4px 12px;
319
+ border-radius: 12px;
320
+ font-size: 12px;
321
+ font-weight: 600;
322
+ text-transform: uppercase;
323
+ }
324
+
325
+ .status-trialing {
326
+ background: #e3f2fd;
327
+ color: #1976d2;
328
+ }
329
+
330
+ .status-active {
331
+ background: #e8f5e9;
332
+ color: #388e3c;
333
+ }
334
+
335
+ .status-past_due {
336
+ background: #fff3e0;
337
+ color: #f57c00;
338
+ }
339
+
340
+ .status-canceled {
341
+ background: #ffebee;
342
+ color: #d32f2f;
343
+ }
344
+
345
+ .subscription-details {
346
+ margin-bottom: 16px;
347
+ }
348
+
349
+ .detail-row {
350
+ display: flex;
351
+ justify-content: space-between;
352
+ padding: 8px 0;
353
+ font-size: 15px;
354
+ }
355
+
356
+ .detail-label {
357
+ color: #666;
358
+ font-weight: 500;
359
+ }
360
+
361
+ .detail-value {
362
+ color: #000;
363
+ font-weight: 600;
364
+ }
365
+
366
+ .subscription-actions {
367
+ display: flex;
368
+ justify-content: flex-end;
369
+ padding-top: 16px;
370
+ border-top: 1px solid #f0f0f0;
371
+ }
372
+
373
+ .cancel-button {
374
+ padding: 10px 20px;
375
+ background: #fff;
376
+ color: #d32f2f;
377
+ border: 1px solid #d32f2f;
378
+ border-radius: 8px;
379
+ font-size: 14px;
380
+ font-weight: 600;
381
+ cursor: pointer;
382
+ transition: all 0.2s;
383
+ }
384
+
385
+ .cancel-button:hover:not(:disabled) {
386
+ background: #d32f2f;
387
+ color: #fff;
388
+ }
389
+
390
+ .cancel-button:disabled {
391
+ opacity: 0.5;
392
+ cursor: not-allowed;
393
+ }
394
+
395
+ /* Modal Styles */
396
+ .modal-overlay {
397
+ position: fixed;
398
+ top: 0;
399
+ left: 0;
400
+ right: 0;
401
+ bottom: 0;
402
+ background: rgba(0, 0, 0, 0.5);
403
+ display: flex;
404
+ align-items: center;
405
+ justify-content: center;
406
+ z-index: 9999;
407
+ padding: 20px;
408
+ }
409
+
410
+ .modal-content {
411
+ background: #fff;
412
+ border-radius: 16px;
413
+ width: 100%;
414
+ max-width: 500px;
415
+ max-height: 90vh;
416
+ overflow-y: auto;
417
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
418
+ }
419
+
420
+ .modal-header {
421
+ display: flex;
422
+ justify-content: space-between;
423
+ align-items: center;
424
+ padding: 24px;
425
+ border-bottom: 1px solid #e0e0e0;
426
+ }
427
+
428
+ .modal-header h3 {
429
+ margin: 0;
430
+ font-size: 20px;
431
+ font-weight: 600;
432
+ color: #000;
433
+ }
434
+
435
+ .close-button {
436
+ background: none;
437
+ border: none;
438
+ font-size: 28px;
439
+ color: #666;
440
+ cursor: pointer;
441
+ padding: 0;
442
+ width: 32px;
443
+ height: 32px;
444
+ display: flex;
445
+ align-items: center;
446
+ justify-content: center;
447
+ border-radius: 50%;
448
+ transition: background 0.2s;
449
+ }
450
+
451
+ .close-button:hover {
452
+ background: #f0f0f0;
453
+ }
454
+
455
+ .modal-body {
456
+ padding: 24px;
457
+ }
458
+
459
+ .modal-text {
460
+ font-size: 16px;
461
+ line-height: 1.5;
462
+ color: #333;
463
+ margin-bottom: 24px;
464
+ }
465
+
466
+ .feedback-section {
467
+ margin-top: 20px;
468
+ }
469
+
470
+ .feedback-label {
471
+ display: block;
472
+ font-size: 14px;
473
+ font-weight: 600;
474
+ color: #333;
475
+ margin-bottom: 8px;
476
+ }
477
+
478
+ .feedback-textarea {
479
+ width: 100%;
480
+ padding: 12px;
481
+ border: 1px solid #e0e0e0;
482
+ border-radius: 8px;
483
+ font-size: 14px;
484
+ font-family: inherit;
485
+ resize: vertical;
486
+ transition: border-color 0.2s;
487
+ }
488
+
489
+ .feedback-textarea:focus {
490
+ outline: none;
491
+ border-color: #1976d2;
492
+ }
493
+
494
+ .modal-footer {
495
+ display: flex;
496
+ gap: 12px;
497
+ padding: 20px 24px;
498
+ border-top: 1px solid #e0e0e0;
499
+ justify-content: flex-end;
500
+ }
501
+
502
+ .button-secondary {
503
+ padding: 12px 24px;
504
+ background: #fff;
505
+ color: #333;
506
+ border: 1px solid #e0e0e0;
507
+ border-radius: 8px;
508
+ font-size: 14px;
509
+ font-weight: 600;
510
+ cursor: pointer;
511
+ transition: all 0.2s;
512
+ }
513
+
514
+ .button-secondary:hover {
515
+ background: #f5f5f5;
516
+ }
517
+
518
+ .button-danger {
519
+ padding: 12px 24px;
520
+ background: #d32f2f;
521
+ color: #fff;
522
+ border: none;
523
+ border-radius: 8px;
524
+ font-size: 14px;
525
+ font-weight: 600;
526
+ cursor: pointer;
527
+ transition: all 0.2s;
528
+ }
529
+
530
+ .button-danger:hover:not(:disabled) {
531
+ background: #b71c1c;
532
+ }
533
+
534
+ .button-danger:disabled {
535
+ opacity: 0.5;
536
+ cursor: not-allowed;
537
+ }
108
538
  </style>