rails_email 0.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.
@@ -0,0 +1,299 @@
1
+ <style>
2
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
3
+
4
+ :root {
5
+ --bg: #0a0a0a;
6
+ --sidebar-bg: #111111;
7
+ --sidebar-hover: #1a1a1a;
8
+ --sidebar-active-bg: rgba(13, 80, 74, 0.45);
9
+ --sidebar-active-text: #7dd3c8;
10
+ --border: #222222;
11
+ --text: #e5e5e5;
12
+ --text-muted: #888888;
13
+ --text-dim: #555555;
14
+ --accent: #2dd4bf;
15
+ --preview-bg: #1c1c1c;
16
+ --btn-hover: rgba(255,255,255,0.06);
17
+ --btn-active-bg: rgba(255,255,255,0.1);
18
+ --nav-height: 44px;
19
+ --sidebar-width: 220px;
20
+ }
21
+
22
+ html[data-theme="light"] {
23
+ --bg: #ffffff;
24
+ --sidebar-bg: #f5f5f5;
25
+ --sidebar-hover: #ececec;
26
+ --sidebar-active-bg: rgba(13, 148, 136, 0.12);
27
+ --sidebar-active-text: #0d7065;
28
+ --border: #e5e5e5;
29
+ --text: #111111;
30
+ --text-muted: #666666;
31
+ --text-dim: #aaaaaa;
32
+ --accent: #0d9488;
33
+ --preview-bg: #e0e0e0;
34
+ --btn-hover: rgba(0,0,0,0.05);
35
+ --btn-active-bg: rgba(0,0,0,0.08);
36
+ }
37
+
38
+ html, body { height: 100%; }
39
+
40
+ body {
41
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
42
+ font-size: 14px;
43
+ background: var(--bg);
44
+ color: var(--text);
45
+ overflow: hidden;
46
+ -webkit-font-smoothing: antialiased;
47
+ }
48
+
49
+ /* ── App shell ─────────────────────────────────────────── */
50
+ .rm-app { display: flex; flex-direction: column; height: 100vh; }
51
+
52
+ /* ── Navbar ─────────────────────────────────────────────── */
53
+ .rm-nav {
54
+ display: flex; align-items: center; justify-content: space-between;
55
+ height: var(--nav-height); padding: 0 12px;
56
+ border-bottom: 1px solid var(--border);
57
+ background: var(--bg); flex-shrink: 0; gap: 8px; z-index: 10;
58
+ }
59
+
60
+ .rm-nav-left { display: flex; align-items: center; gap: 8px; min-width: 180px; }
61
+ .rm-nav-center { display: flex; align-items: center; gap: 10px; flex: 1; justify-content: center; }
62
+ .rm-nav-right { display: flex; align-items: center; gap: 6px; min-width: 180px; justify-content: flex-end; }
63
+
64
+ .rm-logo {
65
+ display: flex; align-items: center; gap: 7px;
66
+ font-size: 13px; font-weight: 500; color: var(--text);
67
+ text-decoration: none; white-space: nowrap;
68
+ }
69
+ .rm-logo svg { color: var(--text-muted); flex-shrink: 0; }
70
+
71
+ .rm-breadcrumb {
72
+ font-family: ui-monospace, "SF Mono", "Cascadia Mono", monospace;
73
+ font-size: 12.5px; color: var(--text-muted);
74
+ }
75
+ .rm-breadcrumb .sep { margin: 0 5px; color: var(--text-dim); }
76
+ .rm-breadcrumb .current { color: var(--text); }
77
+
78
+ /* Toggle pill */
79
+ .rm-toggle {
80
+ display: flex; background: var(--sidebar-bg);
81
+ border: 1px solid var(--border); border-radius: 7px; padding: 2px; gap: 1px;
82
+ }
83
+ .rm-toggle-btn {
84
+ display: flex; align-items: center; justify-content: center;
85
+ width: 30px; height: 24px; border: none; background: transparent;
86
+ color: var(--text-muted); border-radius: 5px; cursor: pointer;
87
+ transition: background 0.12s, color 0.12s;
88
+ }
89
+ .rm-toggle-btn.active { background: var(--btn-active-bg); color: var(--text); }
90
+ .rm-toggle-btn:hover:not(.active) { background: var(--btn-hover); color: var(--text); }
91
+
92
+ /* Icon button */
93
+ .rm-icon-btn {
94
+ display: flex; align-items: center; justify-content: center;
95
+ width: 30px; height: 30px; border: none; background: transparent;
96
+ color: var(--text-muted); border-radius: 6px; cursor: pointer;
97
+ transition: background 0.12s, color 0.12s;
98
+ }
99
+ .rm-icon-btn:hover { background: var(--btn-hover); color: var(--text); }
100
+
101
+ /* Download button */
102
+ .rm-btn-download {
103
+ display: inline-flex; align-items: center; padding: 5px 13px;
104
+ background: var(--text); color: var(--bg);
105
+ border: none; border-radius: 6px; font-size: 12px; font-weight: 500;
106
+ cursor: pointer; text-decoration: none; white-space: nowrap; transition: opacity 0.12s;
107
+ }
108
+ .rm-btn-download:hover { opacity: 0.85; }
109
+
110
+ /* ── Viewport size group + dropdown ─────────────────────── */
111
+ .rm-size-group { position: relative; display: flex; align-items: center; }
112
+
113
+ .rm-viewport-panel {
114
+ position: absolute; top: calc(100% + 8px); right: 0;
115
+ width: 210px; background: var(--sidebar-bg);
116
+ border: 1px solid var(--border); border-radius: 9px;
117
+ padding: 6px 0; z-index: 200;
118
+ box-shadow: 0 8px 28px rgba(0,0,0,0.35);
119
+ }
120
+
121
+ .rm-vp-section {
122
+ padding: 6px 12px 3px; font-size: 11px; font-weight: 600;
123
+ color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.07em;
124
+ }
125
+ .rm-vp-preset {
126
+ display: flex; justify-content: space-between; align-items: center;
127
+ width: 100%; padding: 7px 12px; border: none; background: transparent;
128
+ color: var(--text-muted); font-size: 13px; cursor: pointer; text-align: left;
129
+ transition: background 0.1s, color 0.1s;
130
+ }
131
+ .rm-vp-preset:hover { background: var(--sidebar-hover); color: var(--text); }
132
+ .rm-vp-preset.rm-vp-active { color: var(--accent); }
133
+ .rm-vp-dim { font-size: 11px; color: var(--text-dim); font-variant-numeric: tabular-nums; }
134
+ .rm-vp-divider { height: 1px; background: var(--border); margin: 5px 0; }
135
+ .rm-vp-inputs {
136
+ display: flex; gap: 6px; padding: 4px 12px 6px;
137
+ }
138
+ .rm-vp-input-group { display: flex; align-items: center; gap: 4px; flex: 1; }
139
+ .rm-vp-input-group label { font-size: 11px; color: var(--text-dim); width: 14px; flex-shrink: 0; }
140
+ .rm-vp-input-group input {
141
+ width: 100%; background: var(--bg); border: 1px solid var(--border);
142
+ border-radius: 4px; color: var(--text); padding: 4px 6px; font-size: 12px;
143
+ }
144
+ .rm-vp-input-group input:focus { outline: none; border-color: var(--accent); }
145
+ .rm-vp-input-group span { font-size: 11px; color: var(--text-dim); }
146
+ .rm-vp-apply {
147
+ display: block; width: calc(100% - 24px); margin: 2px 12px 4px;
148
+ padding: 6px; background: var(--accent); color: #000;
149
+ border: none; border-radius: 5px; font-size: 12px; font-weight: 600;
150
+ cursor: pointer;
151
+ }
152
+ .rm-vp-apply:hover { opacity: 0.88; }
153
+
154
+ /* ── Body layout ────────────────────────────────────────── */
155
+ .rm-body { display: flex; flex: 1; overflow: hidden; }
156
+
157
+ /* ── Sidebar ────────────────────────────────────────────── */
158
+ .rm-sidebar {
159
+ width: var(--sidebar-width); background: var(--sidebar-bg);
160
+ border-right: 1px solid var(--border); overflow-y: auto; flex-shrink: 0;
161
+ transition: width 0.18s ease, opacity 0.18s ease;
162
+ }
163
+ .rm-sidebar.collapsed { width: 0; opacity: 0; overflow: hidden; }
164
+
165
+ .rm-mailer-group { padding: 6px 0; }
166
+ .rm-mailer-label {
167
+ padding: 6px 12px 3px; font-size: 12px; font-weight: 600;
168
+ color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.07em;
169
+ }
170
+ .rm-email-link {
171
+ display: flex; align-items: center; gap: 7px; padding: 6px 12px;
172
+ color: var(--text-muted); text-decoration: none;
173
+ font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
174
+ transition: background 0.1s, color 0.1s;
175
+ }
176
+ .rm-email-link:hover { background: var(--sidebar-hover); color: var(--text); }
177
+ .rm-email-link.active { background: var(--sidebar-active-bg); color: var(--sidebar-active-text); }
178
+ .rm-email-link svg { flex-shrink: 0; opacity: 0.55; }
179
+ .rm-email-link.active svg { opacity: 1; }
180
+
181
+ /* ── Main content area ──────────────────────────────────── */
182
+ .rm-main { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; }
183
+
184
+ .rm-view { flex: 1; overflow: hidden; display: flex; flex-direction: column; }
185
+ .rm-view.hidden { display: none !important; }
186
+
187
+ /* Preview stage */
188
+ .rm-stage {
189
+ flex: 1; background: var(--preview-bg);
190
+ display: flex; justify-content: center; align-items: flex-start;
191
+ overflow: auto; position: relative;
192
+ }
193
+ .rm-stage iframe { border: none; flex-shrink: 0; display: block; }
194
+
195
+ /* Source view */
196
+ .rm-source { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
197
+ .rm-source-tabs {
198
+ display: flex; align-items: center; border-bottom: 1px solid var(--border);
199
+ padding: 0 16px; background: var(--sidebar-bg); flex-shrink: 0;
200
+ }
201
+ .rm-source-tab {
202
+ padding: 0 14px; height: 36px; line-height: 36px; font-size: 14px;
203
+ border: none; background: transparent; color: var(--text-muted); cursor: pointer;
204
+ border-bottom: 2px solid transparent; margin-bottom: -1px; transition: color 0.12s;
205
+ }
206
+ .rm-source-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
207
+ .rm-source-tab:hover:not(.active) { color: var(--text); }
208
+ .rm-source-body {
209
+ flex: 1; overflow: auto; padding: 20px 24px;
210
+ font-family: ui-monospace, "SF Mono", "Cascadia Mono", monospace;
211
+ font-size: 13px; line-height: 1.65; color: var(--text);
212
+ white-space: pre-wrap; word-break: break-all; background: var(--bg);
213
+ }
214
+ .rm-source-body.hidden { display: none !important; }
215
+
216
+ /* ── Bottom panel ───────────────────────────────────────── */
217
+ .rm-bottom { border-top: 1px solid var(--border); background: var(--sidebar-bg); flex-shrink: 0; }
218
+ .rm-bottom-bar {
219
+ display: flex; align-items: center; justify-content: space-between;
220
+ height: 37px; padding: 0 16px; border-bottom: 1px solid var(--border);
221
+ }
222
+ .rm-bottom-tabs { display: flex; }
223
+ .rm-bottom-tab {
224
+ padding: 0 14px; height: 37px; line-height: 37px; font-size: 14px;
225
+ border: none; background: transparent; color: var(--text-muted); cursor: pointer;
226
+ border-bottom: 2px solid transparent; margin-bottom: -1px; transition: color 0.12s;
227
+ }
228
+ .rm-bottom-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
229
+ .rm-bottom-tab:hover:not(.active) { color: var(--text); }
230
+ .rm-bottom-actions { display: flex; align-items: center; gap: 2px; }
231
+
232
+ .rm-panel-body { max-height: 220px; overflow-y: auto; padding: 10px 16px; }
233
+ .rm-panel-body.hidden { display: none !important; }
234
+
235
+ .rm-tab-content { display: none; }
236
+ .rm-tab-content.active { display: block; }
237
+
238
+ /* Headers table */
239
+ .rm-headers { width: 100%; border-collapse: collapse; }
240
+ .rm-headers th {
241
+ text-align: right; padding: 4px 14px 4px 0;
242
+ color: var(--text-dim); font-weight: 500;
243
+ font-size: 13px; width: 72px; white-space: nowrap; vertical-align: top;
244
+ }
245
+ .rm-headers td { padding: 4px 0; color: var(--text); font-size: 13px; word-break: break-word; }
246
+
247
+ .rm-select {
248
+ appearance: none; background: var(--bg); color: var(--text);
249
+ border: 1px solid var(--border); border-radius: 4px;
250
+ padding: 2px 8px; font-size: 12px; cursor: pointer;
251
+ }
252
+
253
+ /* ── Compatibility + Spam panels ────────────────────────── */
254
+ .rm-check-loading {
255
+ padding: 16px; color: var(--text-muted); font-size: 13px;
256
+ text-align: center; display: flex; align-items: center; justify-content: center; gap: 8px;
257
+ }
258
+ .rm-check-error { padding: 12px 0; color: #ef4444; font-size: 13px; }
259
+ .rm-check-placeholder { padding: 12px 0; color: var(--text-dim); font-size: 13px; }
260
+
261
+ .rm-check-ok {
262
+ display: flex; flex-direction: column; align-items: center;
263
+ padding: 14px; gap: 5px; text-align: center;
264
+ }
265
+ .rm-check-ok-icon {
266
+ width: 38px; height: 38px; border-radius: 50%; background: #16a34a;
267
+ color: #fff; display: flex; align-items: center; justify-content: center;
268
+ font-size: 15px; font-weight: 700;
269
+ }
270
+ .rm-check-ok-icon--warn { background: #d97706; }
271
+ .rm-check-ok-icon--danger { background: #dc2626; }
272
+ .rm-check-ok strong { font-size: 14px; color: var(--text); }
273
+ .rm-check-ok p { font-size: 13px; color: var(--text-muted); margin: 0; }
274
+
275
+ /* Compat table */
276
+ .rm-compat-table { width: 100%; border-collapse: collapse; }
277
+ .rm-compat-table tr:not(:last-child) td { border-bottom: 1px solid var(--border); }
278
+ .rm-compat-prop {
279
+ padding: 8px 10px 8px 0; color: #f87171; font-weight: 500;
280
+ font-size: 13px; white-space: nowrap; width: 1px;
281
+ }
282
+ .rm-compat-clients { padding: 8px 0; color: var(--text-muted); font-size: 13px; }
283
+
284
+ /* Spam table */
285
+ .rm-spam-table { width: 100%; border-collapse: collapse; margin-top: 8px; }
286
+ .rm-spam-table tr:not(:last-child) td { border-bottom: 1px solid var(--border); }
287
+ .rm-spam-points { padding: 6px 10px 6px 0; color: #f87171; font-weight: 600; font-size: 13px; text-align: right; width: 1px; white-space: nowrap; }
288
+ .rm-spam-desc { padding: 6px 0; color: var(--text-muted); font-size: 13px; }
289
+
290
+ /* Empty state */
291
+ .rm-empty {
292
+ display: flex; flex-direction: column; align-items: center;
293
+ justify-content: center; flex: 1; gap: 8px; color: var(--text-dim);
294
+ }
295
+ .rm-empty h2 { font-size: 15px; font-weight: 500; color: var(--text-muted); }
296
+ .rm-empty p { font-size: 14px; }
297
+
298
+ .hidden { display: none !important; }
299
+ </style>
@@ -0,0 +1,257 @@
1
+ <%
2
+ _html_part = @email.html_part || (@email.mime_type == "text/html" ? @email : nil)
3
+ _text_part = @email.text_part || (@email.mime_type == "text/plain" ? @email : nil)
4
+ _has_html = _html_part.present?
5
+ _has_text = _text_part.present?
6
+ _spam_path = "#{@preview.preview_name}/#{@email_action}"
7
+ _download_url = url_for(action: :download, locale: params[:locale])
8
+ %>
9
+ <!DOCTYPE html>
10
+ <html data-theme="dark">
11
+ <head>
12
+ <meta charset="utf-8">
13
+ <meta name="viewport" content="width=device-width">
14
+ <title><%= "#{@preview.preview_name} / #{@email_action}" %> — rails mail</title>
15
+ <%= render "rails/mailers/styles" %>
16
+ </head>
17
+ <body>
18
+ <div class="rm-app">
19
+
20
+ <%# ── Navbar ──────────────────────────────────────────── %>
21
+ <nav class="rm-nav">
22
+
23
+ <div class="rm-nav-left">
24
+ <a class="rm-logo" href="<%= url_for(controller: "rails/mailers", action: "index") %>">
25
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
26
+ <rect x="2" y="4" width="20" height="16" rx="2"/>
27
+ <path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>
28
+ </svg>
29
+ rails mail
30
+ </a>
31
+ <button class="rm-icon-btn" onclick="rmToggleSidebar()" title="Toggle sidebar">
32
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
33
+ <rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/>
34
+ </svg>
35
+ </button>
36
+ </div>
37
+
38
+ <div class="rm-nav-center">
39
+ <span class="rm-breadcrumb">
40
+ <a href="<%= url_for(controller: "rails/mailers", action: "preview", path: @preview.preview_name) %>" style="color:inherit;text-decoration:none"><%= @preview.preview_name %></a>
41
+ <span class="sep">/</span>
42
+ <span class="current"><%= @email_action %></span>
43
+ </span>
44
+
45
+ <div class="rm-toggle">
46
+ <button class="rm-toggle-btn active" id="rm-btn-preview" onclick="rmSetView('preview')" title="Preview">
47
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
48
+ <rect width="20" height="14" x="2" y="3" rx="2"/><path d="M8 21h8M12 17v4"/>
49
+ </svg>
50
+ </button>
51
+ <button class="rm-toggle-btn" id="rm-btn-source" onclick="rmSetView('source')" title="Source">
52
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
53
+ <polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>
54
+ </svg>
55
+ </button>
56
+ </div>
57
+ </div>
58
+
59
+ <div class="rm-nav-right">
60
+ <button class="rm-icon-btn" onclick="rmToggleTheme()" title="Toggle theme">
61
+ <svg id="rm-icon-moon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
62
+ <path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>
63
+ </svg>
64
+ <svg id="rm-icon-sun" class="hidden" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
65
+ <circle cx="12" cy="12" r="4"/>
66
+ <path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/>
67
+ </svg>
68
+ </button>
69
+
70
+ <%# Size toggle + viewport dropdown %>
71
+ <div class="rm-size-group" id="rm-size-group">
72
+ <div class="rm-toggle">
73
+ <button class="rm-toggle-btn active" id="rm-btn-desktop" onclick="rmSetSize('desktop')" title="Desktop">
74
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
75
+ <rect width="20" height="14" x="2" y="3" rx="2"/><path d="M8 21h8M12 17v4"/>
76
+ </svg>
77
+ </button>
78
+ <button class="rm-toggle-btn" id="rm-btn-mobile" onclick="rmSetSize('mobile')" title="Mobile">
79
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
80
+ <rect width="14" height="20" x="5" y="2" rx="2"/><path d="M12 18h.01"/>
81
+ </svg>
82
+ </button>
83
+ <button class="rm-toggle-btn" id="rm-btn-viewport" onclick="rmToggleViewportDropdown(event)" title="Custom size">
84
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
85
+ <path d="m6 9 6 6 6-6"/>
86
+ </svg>
87
+ </button>
88
+ </div>
89
+
90
+ <%# Viewport dropdown panel %>
91
+ <div class="rm-viewport-panel hidden" id="rm-viewport-panel">
92
+ <div class="rm-vp-section">Presets</div>
93
+ <button class="rm-vp-preset" data-preset="Mobile" onclick="rmApplyPreset('Mobile', 375, 667)"><span>Mobile</span> <span class="rm-vp-dim">375 × 667</span></button>
94
+ <button class="rm-vp-preset" data-preset="Tablet" onclick="rmApplyPreset('Tablet', 768, 1024)"><span>Tablet</span> <span class="rm-vp-dim">768 × 1024</span></button>
95
+ <button class="rm-vp-preset" data-preset="Desktop" onclick="rmApplyPreset('Desktop',1280, 800)"><span>Desktop</span> <span class="rm-vp-dim">1280 × 800</span></button>
96
+ <button class="rm-vp-preset" data-preset="Email" onclick="rmApplyPreset('Email', 600, null)"><span>Email width</span><span class="rm-vp-dim">600 × auto</span></button>
97
+ <button class="rm-vp-preset" data-preset="Full" onclick="rmApplyPreset('Full', null, null)"><span>Full width</span><span class="rm-vp-dim">auto</span></button>
98
+ <div class="rm-vp-divider"></div>
99
+ <div class="rm-vp-section">Custom</div>
100
+ <div class="rm-vp-inputs">
101
+ <div class="rm-vp-input-group">
102
+ <label>W</label>
103
+ <input type="number" id="rm-vp-w" placeholder="auto" min="220" max="2560">
104
+ <span>px</span>
105
+ </div>
106
+ <div class="rm-vp-input-group">
107
+ <label>H</label>
108
+ <input type="number" id="rm-vp-h" placeholder="auto" min="100" max="2000">
109
+ <span>px</span>
110
+ </div>
111
+ </div>
112
+ <button class="rm-vp-apply" onclick="rmApplyCustomViewport()">Apply</button>
113
+ </div>
114
+ </div>
115
+
116
+ <a class="rm-btn-download" href="<%= _download_url %>">Download .eml</a>
117
+ </div>
118
+
119
+ </nav>
120
+
121
+ <%# ── Body ────────────────────────────────────────────── %>
122
+ <div class="rm-body">
123
+
124
+ <%= render "rails/mailers/sidebar", active_preview: @preview, active_action: @email_action %>
125
+
126
+ <main class="rm-main">
127
+
128
+ <%# ── Preview ──────────────────────────────────────── %>
129
+ <div class="rm-view" id="rm-view-preview">
130
+ <div class="rm-stage" id="rm-stage">
131
+ <% if @part && @part.mime_type %>
132
+ <iframe id="rm-email-frame" name="messageBody"
133
+ src="?<%= part_query(@part.mime_type) %>"
134
+ style="width:100%;height:100%;"></iframe>
135
+ <% else %>
136
+ <div class="rm-empty" style="width:100%">
137
+ <h2>No content</h2>
138
+ <p>The <code style="font-family:monospace;font-size:11px">mail</code> method was not called in this preview.</p>
139
+ </div>
140
+ <% end %>
141
+ </div>
142
+ </div>
143
+
144
+ <%# ── Source ───────────────────────────────────────── %>
145
+ <div class="rm-view rm-source hidden" id="rm-view-source">
146
+ <div class="rm-source-tabs">
147
+ <% if _has_html %>
148
+ <button class="rm-source-tab active" onclick="rmSetSourceTab('rm-src-html', this)">HTML</button>
149
+ <% end %>
150
+ <% if _has_text %>
151
+ <button class="rm-source-tab <%= "active" unless _has_html %>" onclick="rmSetSourceTab('rm-src-text', this)">Plain Text</button>
152
+ <% end %>
153
+ </div>
154
+ <% if _has_html %>
155
+ <pre class="rm-source-body" id="rm-src-html"><%= h _html_part.decoded %></pre>
156
+ <% end %>
157
+ <% if _has_text %>
158
+ <pre class="rm-source-body <%= "hidden" if _has_html %>" id="rm-src-text"><%= h _text_part.decoded %></pre>
159
+ <% end %>
160
+ <% unless _has_html || _has_text %>
161
+ <pre class="rm-source-body" id="rm-src-html"><%= h @email.decoded rescue h(@email.to_s) %></pre>
162
+ <% end %>
163
+ </div>
164
+
165
+ <%# ── Bottom panel ─────────────────────────────────── %>
166
+ <div class="rm-bottom">
167
+ <div class="rm-bottom-bar">
168
+ <div class="rm-bottom-tabs">
169
+ <button class="rm-bottom-tab active" onclick="rmSetBottomTab('rm-tab-headers', this)">Headers</button>
170
+ <button class="rm-bottom-tab" onclick="rmSetBottomTab('rm-tab-compat', this); rmRunCompatibilityCheck()">Compatibility</button>
171
+ <button class="rm-bottom-tab" data-spam-path="<%= _spam_path %>" onclick="rmSetBottomTab('rm-tab-spam', this); rmRunSpamCheck(this.dataset.spamPath)">Spam</button>
172
+ <% if @attachments.any? %>
173
+ <button class="rm-bottom-tab" onclick="rmSetBottomTab('rm-tab-attachments', this)">Attachments</button>
174
+ <% end %>
175
+ </div>
176
+ <div class="rm-bottom-actions">
177
+ <button class="rm-icon-btn" onclick="window.location.reload()" title="Reload">
178
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
179
+ <path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/>
180
+ <path d="M21 3v5h-5M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/>
181
+ <path d="M3 21v-5h5"/>
182
+ </svg>
183
+ </button>
184
+ <button class="rm-icon-btn" onclick="rmTogglePanel()" title="Collapse">
185
+ <svg id="rm-collapse-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" style="transition:transform 0.18s">
186
+ <path d="m6 9 6 6 6-6"/>
187
+ </svg>
188
+ </button>
189
+ </div>
190
+ </div>
191
+
192
+ <div class="rm-panel-body" id="rm-panel-body">
193
+
194
+ <%# Headers tab %>
195
+ <div class="rm-tab-content active" id="rm-tab-headers">
196
+ <table class="rm-headers">
197
+ <tr><th>From</th> <td><%= @email.header["from"] %></td></tr>
198
+ <tr><th>To</th> <td><%= @email.header["to"] %></td></tr>
199
+ <tr><th>Subject</th> <td><strong><%= @email.subject %></strong></td></tr>
200
+ <tr><th>Date</th> <td><%= @email.header["date"] || Time.current.rfc2822 %></td></tr>
201
+ <% if @email.reply_to %>
202
+ <tr><th>Reply-To</th><td><%= @email.header["reply-to"] %></td></tr>
203
+ <% end %>
204
+ <% if @email.cc %><tr><th>CC</th><td><%= @email.header["cc"] %></td></tr><% end %>
205
+ <% if @email.bcc %><tr><th>BCC</th><td><%= @email.header["bcc"] %></td></tr><% end %>
206
+ <% if I18n.available_locales.count > 1 %>
207
+ <tr>
208
+ <th>Locale</th>
209
+ <td>
210
+ <select id="locale" class="rm-select" onchange="refreshBody(true)">
211
+ <% I18n.available_locales.each do |locale| %>
212
+ <option <%= I18n.locale == locale ? "selected" : "" %> value="<%= locale_query(locale) %>"><%= locale %></option>
213
+ <% end %>
214
+ </select>
215
+ </td>
216
+ </tr>
217
+ <% end %>
218
+ <% if @part %>
219
+ <span id="mime_type" data-mime-type="<%= part_query(@part.mime_type) %>" style="display:none"></span>
220
+ <% end %>
221
+ </table>
222
+ </div>
223
+
224
+ <%# Compatibility tab %>
225
+ <div class="rm-tab-content" id="rm-tab-compat">
226
+ <div id="rm-compat-content"><p class="rm-check-placeholder">Click Compatibility to run check.</p></div>
227
+ </div>
228
+
229
+ <%# Spam tab %>
230
+ <div class="rm-tab-content" id="rm-tab-spam">
231
+ <div id="rm-spam-content"><p class="rm-check-placeholder">Click Spam to check via SpamAssassin.</p></div>
232
+ </div>
233
+
234
+ <%# Attachments tab %>
235
+ <% if @attachments.any? %>
236
+ <div class="rm-tab-content" id="rm-tab-attachments">
237
+ <table class="rm-headers">
238
+ <% (@attachments.merge(@inline_attachments)).each do |filename, attachment| %>
239
+ <tr>
240
+ <th style="text-align:left;width:auto;padding-right:16px"><%= filename %></th>
241
+ <td><a href="<%= attachment_url(attachment) %>" download="<%= filename %>" style="color:var(--accent);text-decoration:none;font-size:13px">Download</a></td>
242
+ </tr>
243
+ <% end %>
244
+ </table>
245
+ </div>
246
+ <% end %>
247
+
248
+ </div>
249
+ </div>
250
+
251
+ </main>
252
+ </div>
253
+ </div>
254
+
255
+ <%= render "rails/mailers/scripts" %>
256
+ </body>
257
+ </html>
@@ -0,0 +1,73 @@
1
+ <!DOCTYPE html>
2
+ <html data-theme="dark">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width">
6
+ <title>rails mail</title>
7
+ <%= render "rails/mailers/styles" %>
8
+ </head>
9
+ <body>
10
+ <div class="rm-app">
11
+
12
+ <%# ── Navbar ─────────────────────────────────────────── %>
13
+ <nav class="rm-nav">
14
+ <div class="rm-nav-left">
15
+ <a class="rm-logo" href="<%= url_for(controller: 'rails/mailers', action: 'index') %>">
16
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
17
+ <rect x="2" y="4" width="20" height="16" rx="2"/>
18
+ <path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>
19
+ </svg>
20
+ rails mail
21
+ </a>
22
+ <button class="rm-icon-btn" onclick="rmToggleSidebar()" title="Toggle sidebar">
23
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
24
+ <rect width="18" height="18" x="3" y="3" rx="2"/>
25
+ <path d="M9 3v18"/>
26
+ </svg>
27
+ </button>
28
+ </div>
29
+ <div class="rm-nav-center"></div>
30
+ <div class="rm-nav-right">
31
+ <button class="rm-icon-btn" onclick="rmToggleTheme()" title="Toggle theme">
32
+ <svg id="rm-icon-moon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
33
+ <path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>
34
+ </svg>
35
+ <svg id="rm-icon-sun" class="hidden" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
36
+ <circle cx="12" cy="12" r="4"/>
37
+ <path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/>
38
+ </svg>
39
+ </button>
40
+ </div>
41
+ </nav>
42
+
43
+ <%# ── Body ───────────────────────────────────────────── %>
44
+ <div class="rm-body">
45
+ <%= render "rails/mailers/sidebar", active_preview: nil, active_action: nil %>
46
+
47
+ <main class="rm-main">
48
+ <% if @previews.any? %>
49
+ <div class="rm-empty">
50
+ <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" style="color: var(--text-dim)">
51
+ <rect x="2" y="4" width="20" height="16" rx="2"/>
52
+ <path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>
53
+ </svg>
54
+ <h2>Select an email to preview</h2>
55
+ <p><%= @previews.sum { |p| p.emails.count } %> email<%= @previews.sum { |p| p.emails.count } == 1 ? "" : "s" %> across <%= @previews.count %> mailer<%= @previews.count == 1 ? "" : "s" %></p>
56
+ </div>
57
+ <% else %>
58
+ <div class="rm-empty">
59
+ <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" style="color: var(--text-dim)">
60
+ <rect x="2" y="4" width="20" height="16" rx="2"/>
61
+ <path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>
62
+ </svg>
63
+ <h2>No mailer previews found</h2>
64
+ <p>Create preview classes in <code style="font-family: monospace; font-size: 11px;">test/mailers/previews/</code></p>
65
+ </div>
66
+ <% end %>
67
+ </main>
68
+ </div>
69
+
70
+ </div>
71
+ <%= render "rails/mailers/scripts" %>
72
+ </body>
73
+ </html>