sincerely 1.0.0 → 1.1.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.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/app/assets/config/sincerely.js +2 -0
  4. data/app/assets/images/sincerely/arrow_left.svg +1 -0
  5. data/app/assets/images/sincerely/check.svg +3 -0
  6. data/app/assets/images/sincerely/chevron_left.svg +1 -0
  7. data/app/assets/images/sincerely/clock.svg +1 -0
  8. data/app/assets/images/sincerely/code.svg +1 -0
  9. data/app/assets/images/sincerely/eye.svg +1 -0
  10. data/app/assets/images/sincerely/logout.svg +1 -0
  11. data/app/assets/images/sincerely/menu.svg +1 -0
  12. data/app/assets/images/sincerely/moon.svg +1 -0
  13. data/app/assets/images/sincerely/pencil.svg +4 -0
  14. data/app/assets/images/sincerely/plus.svg +3 -0
  15. data/app/assets/images/sincerely/progress_spinner.svg +3 -0
  16. data/app/assets/images/sincerely/spinner.svg +3 -0
  17. data/app/assets/images/sincerely/sun.svg +1 -0
  18. data/app/assets/images/sincerely/warning_circle.svg +4 -0
  19. data/app/assets/javascripts/sincerely/charts.js +115 -0
  20. data/app/assets/javascripts/sincerely/clickable_rows.js +29 -0
  21. data/app/assets/javascripts/sincerely/layout.js +104 -0
  22. data/app/assets/javascripts/sincerely/send_form.js +243 -0
  23. data/app/assets/javascripts/sincerely/template_form.js +44 -0
  24. data/app/assets/javascripts/sincerely/template_preview.js +12 -0
  25. data/app/assets/javascripts/sincerely/theme_init.js +9 -0
  26. data/app/assets/stylesheets/sincerely/charts.css +250 -0
  27. data/app/assets/stylesheets/sincerely/components.css +632 -0
  28. data/app/assets/stylesheets/sincerely/layout.css +233 -0
  29. data/app/assets/stylesheets/sincerely/navigation.css +198 -0
  30. data/app/assets/stylesheets/sincerely/send_form.css +287 -0
  31. data/app/assets/stylesheets/sincerely/template_form.css +74 -0
  32. data/app/assets/stylesheets/sincerely/template_preview.css +75 -0
  33. data/app/assets/stylesheets/sincerely/variables.css +88 -0
  34. data/app/controllers/sincerely/application_controller.rb +3 -72
  35. data/app/controllers/sincerely/concerns/authorization.rb +21 -0
  36. data/app/controllers/sincerely/concerns/delivery_metrics.rb +51 -0
  37. data/app/controllers/sincerely/concerns/event_filtering.rb +58 -0
  38. data/app/controllers/sincerely/concerns/event_timeline.rb +41 -0
  39. data/app/controllers/sincerely/concerns/notification_timeline.rb +73 -0
  40. data/app/controllers/sincerely/concerns/notifications.rb +47 -0
  41. data/app/controllers/sincerely/concerns/pagination.rb +39 -0
  42. data/app/controllers/sincerely/concerns/periods.rb +40 -0
  43. data/app/controllers/sincerely/dashboard_controller.rb +30 -88
  44. data/app/controllers/sincerely/delivery_events_controller.rb +13 -21
  45. data/app/controllers/sincerely/engagement_events_controller.rb +13 -22
  46. data/app/controllers/sincerely/notifications_controller.rb +68 -49
  47. data/app/controllers/sincerely/send_controller.rb +82 -67
  48. data/app/controllers/sincerely/templates_controller.rb +37 -8
  49. data/app/helpers/sincerely/application_helper.rb +22 -0
  50. data/app/helpers/sincerely/chart_helper.rb +79 -0
  51. data/app/views/layouts/sincerely/application.html.erb +24 -528
  52. data/app/views/sincerely/dashboard/index.html.erb +25 -256
  53. data/app/views/sincerely/delivery_events/index.html.erb +6 -38
  54. data/app/views/sincerely/engagement_events/index.html.erb +6 -38
  55. data/app/views/sincerely/notifications/index.html.erb +7 -23
  56. data/app/views/sincerely/notifications/show.html.erb +4 -4
  57. data/app/views/sincerely/send/new.html.erb +8 -522
  58. data/app/views/sincerely/shared/_event.html.erb +21 -0
  59. data/app/views/sincerely/shared/_line_chart.html.erb +38 -0
  60. data/app/views/sincerely/shared/_notification_type_filter.html.erb +9 -0
  61. data/app/views/sincerely/shared/_status_option.html.erb +1 -0
  62. data/app/views/sincerely/shared/_template_option.html.erb +3 -0
  63. data/app/views/sincerely/templates/_form.html.erb +8 -107
  64. data/app/views/sincerely/templates/edit.html.erb +2 -2
  65. data/app/views/sincerely/templates/index.html.erb +7 -15
  66. data/app/views/sincerely/templates/new.html.erb +2 -2
  67. data/app/views/sincerely/templates/preview.html.erb +7 -26
  68. data/app/views/sincerely/templates/show.html.erb +3 -3
  69. data/lib/config/sincerely_config.rb +2 -2
  70. data/lib/sincerely/delivery_systems/email_aws_ses.rb +2 -2
  71. data/lib/sincerely/engine.rb +4 -0
  72. data/lib/sincerely/mixins/notification_model.rb +5 -1
  73. data/lib/sincerely/services/process_delivery_event.rb +9 -7
  74. data/lib/sincerely/templates/notification_template.rb +1 -1
  75. data/lib/sincerely/version.rb +1 -1
  76. metadata +52 -4
  77. data/sincerely.gemspec +0 -37
@@ -0,0 +1,233 @@
1
+ /* Main */
2
+ .main {
3
+ padding: 2rem 0;
4
+ flex: 1;
5
+ overflow-y: auto;
6
+ }
7
+
8
+ @media (max-width: 768px) {
9
+ .main {
10
+ overflow-y: visible;
11
+ }
12
+ }
13
+
14
+ .page-header {
15
+ margin-bottom: 2rem;
16
+ display: none;
17
+ flex-direction: column;
18
+ gap: 0.25rem;
19
+ }
20
+
21
+ .page-header--visible {
22
+ display: flex;
23
+ }
24
+
25
+ .page-header-top {
26
+ display: flex;
27
+ align-items: center;
28
+ gap: 0.75rem;
29
+ }
30
+
31
+ .btn-back {
32
+ display: flex;
33
+ align-items: center;
34
+ justify-content: center;
35
+ width: 2.25rem;
36
+ height: 2.25rem;
37
+ background: none;
38
+ border: 1px solid var(--border-color);
39
+ border-radius: 0.375rem;
40
+ cursor: pointer;
41
+ color: var(--text-secondary);
42
+ transition: all 0.2s;
43
+ }
44
+
45
+ .btn-back:hover {
46
+ background: var(--gray-100);
47
+ color: var(--text-primary);
48
+ }
49
+
50
+ .page-title {
51
+ font-family: 'Jersey 10', cursive;
52
+ font-size: 2rem;
53
+ font-weight: 400;
54
+ color: var(--gray-900);
55
+ }
56
+
57
+ .page-subtitle {
58
+ color: var(--gray-500);
59
+ }
60
+
61
+ /* Grid */
62
+ .grid {
63
+ display: grid;
64
+ gap: 1.5rem;
65
+ }
66
+
67
+ .grid-2 {
68
+ grid-template-columns: repeat(2, 1fr);
69
+ }
70
+
71
+ .grid > * {
72
+ min-width: 0;
73
+ }
74
+
75
+ /* Flash */
76
+ .flash {
77
+ padding: 1rem;
78
+ border-radius: 0.375rem;
79
+ margin-bottom: 1rem;
80
+ }
81
+
82
+ .flash-notice {
83
+ background: var(--success-bg);
84
+ color: var(--success-text);
85
+ }
86
+
87
+ .flash-alert {
88
+ background: var(--danger-bg);
89
+ color: var(--danger-text);
90
+ }
91
+
92
+ /* Return Footer */
93
+ .return-footer {
94
+ position: fixed;
95
+ bottom: 0;
96
+ left: 0;
97
+ right: 0;
98
+ background: var(--bg-primary);
99
+ border-top: 1px solid var(--border-color);
100
+ padding: 0.5rem 1rem;
101
+ z-index: 40;
102
+ opacity: 0.85;
103
+ transition: opacity 0.2s, background-color 0.3s;
104
+ }
105
+
106
+ .return-footer:hover {
107
+ opacity: 1;
108
+ }
109
+
110
+ .return-footer-link {
111
+ display: inline-flex;
112
+ align-items: center;
113
+ gap: 0.375rem;
114
+ color: var(--text-secondary);
115
+ text-decoration: none;
116
+ font-size: 0.8125rem;
117
+ transition: color 0.15s;
118
+ }
119
+
120
+ .return-footer-link:hover {
121
+ color: var(--primary);
122
+ }
123
+
124
+ .return-footer-link svg {
125
+ width: 0.875rem;
126
+ height: 0.875rem;
127
+ }
128
+
129
+ .main.has-return-footer {
130
+ padding-bottom: 3.5rem;
131
+ }
132
+
133
+ /* Responsive */
134
+ @media (max-width: 768px) {
135
+ .grid-2 {
136
+ grid-template-columns: 1fr;
137
+ }
138
+
139
+ .page-header {
140
+ display: flex;
141
+ justify-content: space-between;
142
+ align-items: flex-start;
143
+ }
144
+
145
+ .nav-toggle {
146
+ display: block;
147
+ order: 1;
148
+ position: relative;
149
+ }
150
+
151
+ .nav-brand {
152
+ order: 2;
153
+ }
154
+
155
+ .nav-utils {
156
+ order: 3;
157
+ margin-left: auto;
158
+ }
159
+
160
+ .nav-links {
161
+ display: none;
162
+ flex-direction: column;
163
+ position: absolute;
164
+ top: 100%;
165
+ left: 1rem;
166
+ margin-top: 0.25rem;
167
+ background: var(--bg-primary);
168
+ border: 1px solid var(--border-color);
169
+ border-radius: 0.5rem;
170
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
171
+ min-width: 180px;
172
+ z-index: 100;
173
+ margin-left: 0;
174
+ gap: 0;
175
+ padding: 0;
176
+ }
177
+
178
+ .nav-links.open {
179
+ display: flex;
180
+ }
181
+
182
+ .nav-links a {
183
+ padding: 0.625rem 1rem;
184
+ border-bottom: none;
185
+ color: var(--text-secondary);
186
+ transition: all 0.15s;
187
+ }
188
+
189
+ .nav-links a:first-child {
190
+ border-radius: 0.5rem 0.5rem 0 0;
191
+ }
192
+
193
+ .nav-links a:last-child {
194
+ border-radius: 0 0 0.5rem 0.5rem;
195
+ }
196
+
197
+ .nav-links a:hover {
198
+ color: var(--text-primary);
199
+ }
200
+
201
+ .nav-links a.active {
202
+ color: var(--primary);
203
+ font-weight: 600;
204
+ }
205
+
206
+ .stats-grid {
207
+ grid-template-columns: repeat(2, 1fr);
208
+ }
209
+
210
+ .stat-value {
211
+ font-size: 1.5rem;
212
+ }
213
+
214
+ .card {
215
+ padding: 1rem;
216
+ }
217
+
218
+ .card-header {
219
+ flex-direction: column;
220
+ align-items: flex-start;
221
+ gap: 0.5rem;
222
+ }
223
+
224
+ .main {
225
+ padding: 1rem 0;
226
+ }
227
+ }
228
+
229
+ @media (max-width: 480px) {
230
+ .stats-grid {
231
+ grid-template-columns: 1fr;
232
+ }
233
+ }
@@ -0,0 +1,198 @@
1
+ /* Navigation */
2
+ .nav {
3
+ background: var(--bg-primary);
4
+ border-bottom: 1px solid var(--border-color);
5
+ padding: 1rem 0;
6
+ transition: background-color 0.3s;
7
+ z-index: 50;
8
+ box-shadow: 0 1px 3px rgba(0,0,0,0.08);
9
+ flex-shrink: 0;
10
+ }
11
+
12
+ .nav-inner {
13
+ display: flex;
14
+ align-items: center;
15
+ flex-wrap: wrap;
16
+ gap: 0.5rem;
17
+ position: relative;
18
+ }
19
+
20
+ .nav-brand {
21
+ font-family: 'Jersey 10', cursive;
22
+ font-weight: 400;
23
+ font-size: 2.25rem;
24
+ color: var(--text-secondary);
25
+ text-decoration: none;
26
+ }
27
+
28
+ [data-theme="dark"] .nav-brand {
29
+ color: #9393a1;
30
+ }
31
+
32
+ .nav-toggle {
33
+ display: none;
34
+ background: none;
35
+ border: 1px solid var(--border-color);
36
+ border-radius: 0.375rem;
37
+ padding: 0.5rem;
38
+ cursor: pointer;
39
+ color: var(--text-secondary);
40
+ height: 2.25rem;
41
+ align-items: center;
42
+ justify-content: center;
43
+ }
44
+
45
+ .nav-toggle svg {
46
+ width: 1.25rem;
47
+ height: 1.25rem;
48
+ display: block;
49
+ }
50
+
51
+ .nav-links {
52
+ display: flex;
53
+ gap: 1.5rem;
54
+ align-items: center;
55
+ margin-left: 2rem;
56
+ }
57
+
58
+ .nav-links a {
59
+ color: var(--text-secondary);
60
+ text-decoration: none;
61
+ font-weight: 500;
62
+ padding: 0.5rem 0;
63
+ border-bottom: 2px solid transparent;
64
+ }
65
+
66
+ .nav-links a:hover,
67
+ .nav-links a.active {
68
+ color: var(--primary);
69
+ border-bottom-color: var(--primary);
70
+ }
71
+
72
+ .nav-utils {
73
+ display: flex;
74
+ align-items: center;
75
+ gap: 0.5rem;
76
+ margin-left: auto;
77
+ }
78
+
79
+ /* Theme Toggle */
80
+ .theme-toggle {
81
+ background: none;
82
+ border: 1px solid var(--border-color);
83
+ border-radius: 0.375rem;
84
+ padding: 0.5rem;
85
+ cursor: pointer;
86
+ color: var(--text-secondary);
87
+ display: flex;
88
+ align-items: center;
89
+ justify-content: center;
90
+ transition: all 0.2s;
91
+ height: 2.25rem;
92
+ }
93
+
94
+ .theme-toggle:hover {
95
+ background: var(--gray-100);
96
+ color: var(--text-primary);
97
+ }
98
+
99
+ .theme-toggle svg {
100
+ width: 1.25rem;
101
+ height: 1.25rem;
102
+ }
103
+
104
+ .theme-toggle .icon-sun {
105
+ display: none;
106
+ }
107
+
108
+ .theme-toggle .icon-moon {
109
+ display: block;
110
+ }
111
+
112
+ [data-theme="dark"] .theme-toggle .icon-sun {
113
+ display: block;
114
+ }
115
+
116
+ [data-theme="dark"] .theme-toggle .icon-moon {
117
+ display: none;
118
+ }
119
+
120
+ /* Time Filter Dropdown */
121
+ .time-dropdown {
122
+ position: relative;
123
+ }
124
+
125
+ .time-dropdown-btn {
126
+ display: flex;
127
+ align-items: center;
128
+ gap: 0.375rem;
129
+ background: none;
130
+ border: 1px solid var(--border-color);
131
+ border-radius: 0.375rem;
132
+ padding: 0 0.625rem;
133
+ cursor: pointer;
134
+ color: var(--text-secondary);
135
+ font-size: 0.8125rem;
136
+ font-weight: 500;
137
+ transition: all 0.2s;
138
+ height: 2.25rem;
139
+ }
140
+
141
+ .time-dropdown-btn:hover {
142
+ background: var(--gray-100);
143
+ color: var(--text-primary);
144
+ }
145
+
146
+ .time-dropdown-btn svg {
147
+ width: 1rem;
148
+ height: 1rem;
149
+ }
150
+
151
+ .time-dropdown-menu {
152
+ position: absolute;
153
+ top: 100%;
154
+ right: 0;
155
+ margin-top: 0.25rem;
156
+ background: var(--bg-primary);
157
+ border: 1px solid var(--border-color);
158
+ border-radius: 0.5rem;
159
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
160
+ min-width: 140px;
161
+ z-index: 100;
162
+ display: none;
163
+ }
164
+
165
+ .time-dropdown.open .time-dropdown-menu {
166
+ display: block;
167
+ }
168
+
169
+ .time-dropdown-item {
170
+ display: block;
171
+ width: 100%;
172
+ padding: 0.5rem 0.75rem;
173
+ border: none;
174
+ background: none;
175
+ text-align: left;
176
+ font-size: 0.8125rem;
177
+ color: var(--text-secondary);
178
+ cursor: pointer;
179
+ transition: all 0.15s;
180
+ }
181
+
182
+ .time-dropdown-item:first-child {
183
+ border-radius: 0.5rem 0.5rem 0 0;
184
+ }
185
+
186
+ .time-dropdown-item:last-child {
187
+ border-radius: 0 0 0.5rem 0.5rem;
188
+ }
189
+
190
+ .time-dropdown-item:hover {
191
+ background: var(--gray-100);
192
+ color: var(--text-primary);
193
+ }
194
+
195
+ .time-dropdown-item.active {
196
+ background: var(--primary);
197
+ color: white;
198
+ }
@@ -0,0 +1,287 @@
1
+ .recipients-limit {
2
+ font-size: 0.75rem;
3
+ color: var(--gray-500);
4
+ background: var(--gray-100);
5
+ padding: 0.25rem 0.5rem;
6
+ border-radius: 0.25rem;
7
+ }
8
+
9
+ .send-form {
10
+ position: relative;
11
+ }
12
+
13
+ .tags-input-container {
14
+ display: flex;
15
+ flex-wrap: wrap;
16
+ gap: 0.375rem;
17
+ padding: 0.5rem;
18
+ border: 1px solid var(--border-color);
19
+ border-radius: 0.375rem;
20
+ background: var(--bg-primary);
21
+ min-height: 2.75rem;
22
+ cursor: text;
23
+ transition: border-color 0.15s, box-shadow 0.15s;
24
+ }
25
+
26
+ .tags-input-container:focus-within {
27
+ border-color: var(--primary);
28
+ box-shadow: 0 0 0 3px rgba(212, 24, 61, 0.1);
29
+ }
30
+
31
+ .tags-list {
32
+ display: flex;
33
+ flex-wrap: wrap;
34
+ gap: 0.375rem;
35
+ }
36
+
37
+ .tag {
38
+ display: inline-flex;
39
+ align-items: center;
40
+ gap: 0.25rem;
41
+ padding: 0.25rem 0.5rem;
42
+ background: var(--gray-100);
43
+ border-radius: 0.25rem;
44
+ font-size: 0.8125rem;
45
+ color: var(--text-primary);
46
+ animation: tagIn 0.15s ease;
47
+ }
48
+
49
+ @keyframes tagIn {
50
+ from {
51
+ opacity: 0;
52
+ transform: scale(0.9);
53
+ }
54
+ to {
55
+ opacity: 1;
56
+ transform: scale(1);
57
+ }
58
+ }
59
+
60
+ .tag.invalid {
61
+ background: var(--danger-bg);
62
+ color: var(--danger-text);
63
+ }
64
+
65
+ .tag-remove {
66
+ display: flex;
67
+ align-items: center;
68
+ justify-content: center;
69
+ width: 1rem;
70
+ height: 1rem;
71
+ border: none;
72
+ background: none;
73
+ cursor: pointer;
74
+ color: var(--gray-500);
75
+ border-radius: 50%;
76
+ transition: all 0.15s;
77
+ }
78
+
79
+ .tag-remove:hover {
80
+ background: var(--gray-200);
81
+ color: var(--text-primary);
82
+ }
83
+
84
+ .tag-remove svg {
85
+ width: 0.75rem;
86
+ height: 0.75rem;
87
+ }
88
+
89
+ .tags-input {
90
+ flex: 1;
91
+ min-width: 150px;
92
+ border: none;
93
+ outline: none;
94
+ font-size: 0.875rem;
95
+ background: transparent;
96
+ color: var(--text-primary);
97
+ }
98
+
99
+ .tags-input::placeholder {
100
+ color: var(--gray-500);
101
+ }
102
+
103
+ .recipients-count {
104
+ margin-top: 0.5rem;
105
+ font-size: 0.75rem;
106
+ color: var(--gray-500);
107
+ text-align: right;
108
+ }
109
+
110
+ .recipients-count.warning {
111
+ color: var(--warning-text);
112
+ }
113
+
114
+ .recipients-count.limit {
115
+ color: var(--danger-text);
116
+ }
117
+
118
+ .template-variables {
119
+ animation: slideDown 0.2s ease;
120
+ }
121
+
122
+ @keyframes slideDown {
123
+ from {
124
+ opacity: 0;
125
+ transform: translateY(-0.5rem);
126
+ }
127
+ to {
128
+ opacity: 1;
129
+ transform: translateY(0);
130
+ }
131
+ }
132
+
133
+ .variables-grid {
134
+ display: grid;
135
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
136
+ gap: 1rem;
137
+ }
138
+
139
+ .variable-field label {
140
+ display: block;
141
+ font-size: 0.75rem;
142
+ font-weight: 500;
143
+ color: var(--gray-500);
144
+ margin-bottom: 0.25rem;
145
+ }
146
+
147
+ .variable-field input {
148
+ width: 100%;
149
+ padding: 0.5rem 0.75rem;
150
+ border: 1px solid var(--border-color);
151
+ border-radius: 0.375rem;
152
+ font-size: 0.875rem;
153
+ background: var(--bg-primary);
154
+ color: var(--text-primary);
155
+ transition: border-color 0.15s, box-shadow 0.15s;
156
+ }
157
+
158
+ .variable-field input:focus {
159
+ outline: none;
160
+ border-color: var(--primary);
161
+ box-shadow: 0 0 0 3px rgba(212, 24, 61, 0.1);
162
+ }
163
+
164
+ .btn-loader {
165
+ display: inline-flex;
166
+ align-items: center;
167
+ gap: 0.5rem;
168
+ }
169
+
170
+ .spinner {
171
+ width: 1rem;
172
+ height: 1rem;
173
+ animation: spin 1s linear infinite;
174
+ }
175
+
176
+ @keyframes spin {
177
+ from {
178
+ transform: rotate(0deg);
179
+ }
180
+ to {
181
+ transform: rotate(360deg);
182
+ }
183
+ }
184
+
185
+ .send-overlay {
186
+ position: absolute;
187
+ inset: 0;
188
+ background: rgba(255, 255, 255, 0.9);
189
+ display: flex;
190
+ align-items: center;
191
+ justify-content: center;
192
+ border-radius: 0.5rem;
193
+ opacity: 0;
194
+ visibility: hidden;
195
+ transition: opacity 0.3s, visibility 0.3s;
196
+ }
197
+
198
+ [data-theme="dark"] .send-overlay {
199
+ background: rgba(30, 30, 30, 0.9);
200
+ }
201
+
202
+ .send-overlay.active {
203
+ opacity: 1;
204
+ visibility: visible;
205
+ }
206
+
207
+ .send-progress {
208
+ display: flex;
209
+ flex-direction: column;
210
+ align-items: center;
211
+ gap: 1rem;
212
+ }
213
+
214
+ .progress-spinner {
215
+ width: 3rem;
216
+ height: 3rem;
217
+ animation: spin 1s linear infinite;
218
+ }
219
+
220
+ .progress-spinner circle {
221
+ stroke: var(--primary);
222
+ stroke-dasharray: 90;
223
+ stroke-dashoffset: 30;
224
+ stroke-linecap: round;
225
+ }
226
+
227
+ .progress-text {
228
+ font-size: 0.875rem;
229
+ color: var(--text-secondary);
230
+ }
231
+
232
+ .send-result {
233
+ display: flex;
234
+ flex-direction: column;
235
+ align-items: center;
236
+ gap: 1rem;
237
+ padding: 2rem;
238
+ text-align: center;
239
+ animation: fadeIn 0.3s ease;
240
+ }
241
+
242
+ @keyframes fadeIn {
243
+ from {
244
+ opacity: 0;
245
+ }
246
+ to {
247
+ opacity: 1;
248
+ }
249
+ }
250
+
251
+ .result-icon {
252
+ width: 4rem;
253
+ height: 4rem;
254
+ border-radius: 50%;
255
+ display: flex;
256
+ align-items: center;
257
+ justify-content: center;
258
+ }
259
+
260
+ .result-icon svg {
261
+ width: 2rem;
262
+ height: 2rem;
263
+ }
264
+
265
+ .result-success {
266
+ background: var(--success-bg);
267
+ color: var(--success-text);
268
+ }
269
+
270
+ .result-partial {
271
+ background: var(--warning-bg);
272
+ color: var(--warning-text);
273
+ display: none;
274
+ }
275
+
276
+ .result-message {
277
+ font-size: 1rem;
278
+ color: var(--text-primary);
279
+ }
280
+
281
+ .result-message strong {
282
+ color: var(--success);
283
+ }
284
+
285
+ .result-message .failed {
286
+ color: var(--danger);
287
+ }