studio-engine 0.41.0 → 0.42.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +97 -0
- data/README.md +55 -1
- data/app/assets/images/emails/logo-horizontal.png +0 -0
- data/app/assets/images/emails/magic-link-background.gif +0 -0
- data/app/assets/images/emails/newsletter-subscribed-background.gif +0 -0
- data/app/controllers/studio/emails_controller.rb +136 -6
- data/app/mailers/studio/newsletter_mailer.rb +43 -0
- data/app/mailers/user_mailer.rb +48 -1
- data/app/models/studio/email_setting.rb +131 -0
- data/app/services/studio/banner.rb +186 -0
- data/app/services/studio/email_catalog.rb +248 -19
- data/app/services/studio/email_preview_target.rb +195 -0
- data/app/views/layouts/branded_mailer.html.erb +23 -6
- data/app/views/studio/emails/_banner_editor.html.erb +192 -0
- data/app/views/studio/emails/_banner_scale.html.erb +56 -0
- data/app/views/studio/emails/_recipient_picker.html.erb +63 -0
- data/app/views/studio/emails/_recipient_repaint.html.erb +122 -0
- data/app/views/studio/emails/_row.html.erb +68 -4
- data/app/views/studio/emails/index.html.erb +30 -4
- data/app/views/studio/emails/orphan.html.erb +45 -0
- data/app/views/studio/emails/show.html.erb +364 -31
- data/app/views/studio/mailers/_layered_banner.html.erb +136 -0
- data/app/views/studio/modals/_image_upload.html.erb +55 -3
- data/app/views/studio/newsletter_mailer/subscribed.html.erb +33 -0
- data/app/views/studio/newsletter_mailer/subscribed.text.erb +13 -0
- data/app/views/user_mailer/magic_link.html.erb +13 -4
- data/db/migrate/20260812000000_create_studio_email_settings.rb +26 -0
- data/db/migrate/20260812210000_add_copy_to_studio_email_settings.rb +32 -0
- data/db/migrate/20260812220000_add_subject_to_studio_email_settings.rb +15 -0
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +12 -0
- metadata +20 -2
|
@@ -44,6 +44,16 @@
|
|
|
44
44
|
maxWidth: 256,
|
|
45
45
|
maxHeight: 256,
|
|
46
46
|
transparent: true,
|
|
47
|
+
// GIFS BYPASS THE CROPPER ENTIRELY when allowed. They cannot go through
|
|
48
|
+
// it: confirm() paints the crop onto a canvas and calls toBlob(...,
|
|
49
|
+
// "image/png"), which keeps frame one and throws the animation away —
|
|
50
|
+
// silently, with a perfectly good-looking still as the result. So an
|
|
51
|
+
// allowed GIF is passed through as its ORIGINAL BYTES, uncropped, and the
|
|
52
|
+
// aspect it was authored at is the aspect that ships.
|
|
53
|
+
//
|
|
54
|
+
// Off by default because most upload sites (avatars, square thumbnails)
|
|
55
|
+
// genuinely want the crop, and an un-cropped image there is a bug.
|
|
56
|
+
allowGifs: false,
|
|
47
57
|
autoCropArea: 0.9,
|
|
48
58
|
// dispatch: keep the modal open after confirm so the opener's host can run
|
|
49
59
|
// its own processing -> success flow (it replaces the modal). When false
|
|
@@ -57,6 +67,7 @@
|
|
|
57
67
|
if (props.maxWidth) this.maxWidth = props.maxWidth;
|
|
58
68
|
if (props.maxHeight) this.maxHeight = props.maxHeight;
|
|
59
69
|
if (typeof props.transparent === "boolean") this.transparent = props.transparent;
|
|
70
|
+
if (typeof props.allowGifs === "boolean") this.allowGifs = props.allowGifs;
|
|
60
71
|
if (props.dispatch) this.dispatch = true;
|
|
61
72
|
// Carried through untouched and echoed back on confirm, so the host that
|
|
62
73
|
// OPENED the cropper is the only one that acts on the result.
|
|
@@ -103,6 +114,17 @@
|
|
|
103
114
|
return;
|
|
104
115
|
}
|
|
105
116
|
var self = this;
|
|
117
|
+
|
|
118
|
+
if (this.isAnimatedCandidate(file)) {
|
|
119
|
+
if (!this.allowGifs) {
|
|
120
|
+
this.error = "GIFs aren't accepted here. Use a PNG, JPG or WebP.";
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
this.error = null;
|
|
124
|
+
this.passThrough(file);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
106
128
|
var reader = new FileReader();
|
|
107
129
|
reader.onload = function (e) {
|
|
108
130
|
self.error = null;
|
|
@@ -112,6 +134,25 @@
|
|
|
112
134
|
reader.readAsDataURL(file);
|
|
113
135
|
},
|
|
114
136
|
|
|
137
|
+
// Type, not extension: a file renamed to .gif is still a PNG and belongs in
|
|
138
|
+
// the cropper, and a .GIF from a Windows machine is still a GIF.
|
|
139
|
+
isAnimatedCandidate(file) {
|
|
140
|
+
return (file.type || "").toLowerCase() === "image/gif";
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
// Hand the ORIGINAL file straight to the opener, no canvas in the path.
|
|
144
|
+
// Same event and the same owner token as a cropped confirm, so every host
|
|
145
|
+
// downstream is unchanged — the only difference is what is in the blob.
|
|
146
|
+
passThrough(file) {
|
|
147
|
+
try {
|
|
148
|
+
window.dispatchEvent(new CustomEvent("crop-photo-confirmed",
|
|
149
|
+
{ detail: { blob: file, owner: this.owner || null,
|
|
150
|
+
filename: file.name, uncropped: true } }));
|
|
151
|
+
} catch (_) {}
|
|
152
|
+
if (this.cropper) { this.cropper.destroy(); this.cropper = null; }
|
|
153
|
+
if (!this.dispatch) this.$store[this._storeName].close();
|
|
154
|
+
},
|
|
155
|
+
|
|
115
156
|
onFilePicked(event) {
|
|
116
157
|
var file = event.target.files[0];
|
|
117
158
|
event.target.value = "";
|
|
@@ -228,6 +269,7 @@
|
|
|
228
269
|
if (opts.maxWidth) p.maxWidth = opts.maxWidth;
|
|
229
270
|
if (opts.maxHeight) p.maxHeight = opts.maxHeight;
|
|
230
271
|
if (opts.autoCropArea) p.autoCropArea = opts.autoCropArea;
|
|
272
|
+
if (typeof opts.allowGifs === "boolean") p.allowGifs = opts.allowGifs;
|
|
231
273
|
if (extra) { for (var k in extra) { p[k] = extra[k]; } }
|
|
232
274
|
return p;
|
|
233
275
|
}
|
|
@@ -249,7 +291,12 @@
|
|
|
249
291
|
onCropConfirmed(detail) {
|
|
250
292
|
if (!detail) return;
|
|
251
293
|
if (detail.owner && detail.owner !== ownerId) return;
|
|
252
|
-
|
|
294
|
+
// An uncropped pass-through (a GIF) carries its own name and type. The
|
|
295
|
+
// cropped path does not — it produced a PNG — so it keeps the caller's
|
|
296
|
+
// filename. Sending "banner.png" for GIF bytes would have the server
|
|
297
|
+
// store an animated file under a name and content type that say
|
|
298
|
+
// otherwise, and the inbox would be told it is a PNG.
|
|
299
|
+
this.applyCrop(detail.blob, detail.filename);
|
|
253
300
|
},
|
|
254
301
|
// Native picker: read the chosen image, then hand it to the modal.
|
|
255
302
|
onFileSelected(event) {
|
|
@@ -262,10 +309,15 @@
|
|
|
262
309
|
reader.readAsDataURL(file);
|
|
263
310
|
event.target.value = "";
|
|
264
311
|
},
|
|
265
|
-
applyCrop(blob) {
|
|
312
|
+
applyCrop(blob, filename) {
|
|
266
313
|
if (!blob) return;
|
|
314
|
+
// The blob's OWN type when it has one — a pass-through GIF arrives as the
|
|
315
|
+
// original File and already knows it is image/gif. Only the cropped path
|
|
316
|
+
// needs the png default, because that is what toBlob produced.
|
|
317
|
+
var type = blob.type || "image/png";
|
|
318
|
+
var name = filename || opts.filename || "image.png";
|
|
267
319
|
var dt = new DataTransfer();
|
|
268
|
-
dt.items.add(new File([blob],
|
|
320
|
+
dt.items.add(new File([blob], name, { type: type }));
|
|
269
321
|
this.$refs.fileInput.files = dt.files;
|
|
270
322
|
window.submitFormWithProgress(this.$refs.form, opts);
|
|
271
323
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<%# Body only — branded_mailer supplies the banner + card. Colours mirror
|
|
2
|
+
user_mailer/magic_link: brand primary for anything actionable, so the two
|
|
3
|
+
emails read as coming from the same place. %>
|
|
4
|
+
<h1 style="margin:0 0 18px;font-size:22px;line-height:1.3;color:#1f2a1c;">You're on the list</h1>
|
|
5
|
+
|
|
6
|
+
<p style="margin:0 0 24px;font-size:16px;line-height:1.6;color:#3f4a3c;">
|
|
7
|
+
Thanks for subscribing to <strong style="color:#1f2a1c;"><%= @app_name %></strong>. We'll send
|
|
8
|
+
the occasional note about what we're building — no more often than it's worth your time.
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p style="margin:0 0 24px;font-size:16px;line-height:1.6;color:#3f4a3c;">
|
|
12
|
+
Nothing else to do. The next one lands in this inbox.
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<p style="margin:28px 0 0;font-size:13px;line-height:1.5;color:#6b756a;text-align:center;">
|
|
16
|
+
We're sending to <strong style="color:#2f3a2c;"><%= @email %></strong>.
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<% if @unsubscribe_url.present? %>
|
|
20
|
+
<%# Renders ONLY when the host wired a real URL. A newsletter needs a way out,
|
|
21
|
+
but an unsubscribe link that goes nowhere is worse than none — it spends
|
|
22
|
+
the reader's trust and then fails them. %>
|
|
23
|
+
<p style="margin:24px 0 0;font-size:12px;line-height:1.4;color:#8a948a;text-align:center;">
|
|
24
|
+
Changed your mind?
|
|
25
|
+
<a href="<%= @unsubscribe_url %>" style="color:<%= Studio.theme_primary %>;">Unsubscribe</a>.
|
|
26
|
+
</p>
|
|
27
|
+
<% end %>
|
|
28
|
+
|
|
29
|
+
<p style="margin:28px 0 0;font-size:12px;line-height:1.5;color:#8a948a;text-align:center;">
|
|
30
|
+
Didn't sign up? You can safely ignore this email and we won't send another.
|
|
31
|
+
</p>
|
|
32
|
+
|
|
33
|
+
<p style="margin:20px 0 0;font-size:13px;color:#6b756a;text-align:center;">— <%= @app_name %></p>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
You're on the <%= @app_name %> list.
|
|
2
|
+
|
|
3
|
+
Thanks for subscribing. We'll send the occasional note about what we're
|
|
4
|
+
building — no more often than it's worth your time.
|
|
5
|
+
|
|
6
|
+
We're sending to <%= @email %>.
|
|
7
|
+
<% if @unsubscribe_url.present? %>
|
|
8
|
+
Changed your mind? Unsubscribe here:
|
|
9
|
+
<%= @unsubscribe_url %>
|
|
10
|
+
<% end %>
|
|
11
|
+
Didn't sign up? You can safely ignore this message and we won't send another.
|
|
12
|
+
|
|
13
|
+
— <%= @app_name %>
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
<%# Body only — branded_mailer supplies the banner + card.
|
|
2
|
-
|
|
1
|
+
<%# Body only — branded_mailer supplies the banner + card.
|
|
2
|
+
|
|
3
|
+
The button is theme PRIMARY, not success. Success is the "it worked" green;
|
|
4
|
+
signing in has not happened yet, and the brand colour is what ties the button
|
|
5
|
+
to the banner above it. The copy-and-paste link is the SAME colour on purpose
|
|
6
|
+
— it is the button's fallback, and a green link under a purple button reads
|
|
7
|
+
as a mistake. %>
|
|
3
8
|
<h1 style="margin:0 0 18px;font-size:22px;line-height:1.3;color:#1f2a1c;">Your sign-in link</h1>
|
|
4
9
|
|
|
5
10
|
<p style="margin:0 0 24px;font-size:16px;line-height:1.6;color:#3f4a3c;">
|
|
@@ -8,7 +13,7 @@
|
|
|
8
13
|
</p>
|
|
9
14
|
|
|
10
15
|
<table role="presentation" cellpadding="0" cellspacing="0" border="0" align="center" style="margin:0 auto;">
|
|
11
|
-
<tr><td align="center" bgcolor="<%= Studio.
|
|
16
|
+
<tr><td align="center" bgcolor="<%= Studio.theme_primary %>" style="border-radius:10px;">
|
|
12
17
|
<a href="<%= @magic_url %>" style="display:inline-block;padding:16px 40px;font-size:17px;font-weight:700;color:#ffffff;text-decoration:none;border-radius:10px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;">Sign in to <%= @app_name %> 🪄</a>
|
|
13
18
|
</td></tr>
|
|
14
19
|
</table>
|
|
@@ -19,7 +24,11 @@
|
|
|
19
24
|
|
|
20
25
|
<p style="margin:24px 0 0;font-size:12px;line-height:1.4;color:#8a948a;text-align:center;">
|
|
21
26
|
Button not working? Copy and paste this link:<br>
|
|
22
|
-
|
|
27
|
+
<%# 11px, not the old 9px: this is the link someone falls back to when the
|
|
28
|
+
button fails, so it has to be readable enough to select by hand. It stays
|
|
29
|
+
a step under the 12px label above it — the label explains, the URL is the
|
|
30
|
+
thing you drag across. %>
|
|
31
|
+
<a href="<%= @magic_url %>" style="font-size:11px;line-height:1.5;color:<%= Studio.theme_primary %>;word-break:break-all;"><%= @magic_url %></a>
|
|
23
32
|
</p>
|
|
24
33
|
|
|
25
34
|
<p style="margin:28px 0 0;font-size:12px;line-height:1.5;color:#8a948a;text-align:center;">
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Per-email, per-APP settings an operator can change from /admin/emails without
|
|
2
|
+
# a deploy — starting with the banner scrim.
|
|
3
|
+
#
|
|
4
|
+
# Separate from ImageCache (which holds the uploaded banner) because this is a
|
|
5
|
+
# NUMBER, not an asset, and separate from the registry (which is code) because
|
|
6
|
+
# the whole point is that an operator can tune it against real artwork and see
|
|
7
|
+
# the result. The registry stays the default; a row here is an override.
|
|
8
|
+
class CreateStudioEmailSettings < ActiveRecord::Migration[7.2]
|
|
9
|
+
def change
|
|
10
|
+
create_table :studio_email_settings do |t|
|
|
11
|
+
# The registry key — "magic_link", "contest_winnings". Not a foreign key:
|
|
12
|
+
# the catalogue is code, and an app may unregister an email without
|
|
13
|
+
# wanting its saved settings destroyed.
|
|
14
|
+
t.string :email_key, null: false
|
|
15
|
+
|
|
16
|
+
# Scrim as a PERCENT (0-100) rather than a float. It is what the operator
|
|
17
|
+
# types and what the page shows; storing the same units the human uses
|
|
18
|
+
# keeps the round-trip lossless and the value obvious in the console.
|
|
19
|
+
t.integer :scrim_percent
|
|
20
|
+
|
|
21
|
+
t.timestamps
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
add_index :studio_email_settings, :email_key, unique: true
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# The banner's WORDS, editable by the operator alongside its tint.
|
|
2
|
+
#
|
|
3
|
+
# Separate migration rather than an edit to CreateStudioEmailSettings: that one
|
|
4
|
+
# has already been installed and run in a host, and rewriting an applied
|
|
5
|
+
# migration is how a schema quietly diverges from the file that claims to
|
|
6
|
+
# describe it.
|
|
7
|
+
#
|
|
8
|
+
# Every column is nullable, and nil means INHERIT — the registry default applies.
|
|
9
|
+
# That distinction is the whole design: "the operator has not set this" and "the
|
|
10
|
+
# operator set this to empty" are different answers, and a NOT NULL default
|
|
11
|
+
# would collapse them.
|
|
12
|
+
class AddCopyToStudioEmailSettings < ActiveRecord::Migration[7.2]
|
|
13
|
+
def change
|
|
14
|
+
change_table :studio_email_settings, bulk: true do |t|
|
|
15
|
+
# Supports a {name} placeholder, which is what keeps the field honest for
|
|
16
|
+
# an email whose greeting is per-recipient.
|
|
17
|
+
t.string :header
|
|
18
|
+
|
|
19
|
+
# Used when no name is known. A magic link may be the first contact we
|
|
20
|
+
# ever have with someone, so "Welcome {name}!" has to have somewhere to
|
|
21
|
+
# fall back to that is not "Welcome !".
|
|
22
|
+
t.string :header_fallback
|
|
23
|
+
|
|
24
|
+
t.string :subtext
|
|
25
|
+
|
|
26
|
+
# Blank inherits the registry logo. hide_logo is the deliberate "no logo"
|
|
27
|
+
# answer — without it, blank would have to mean both inherit and none.
|
|
28
|
+
t.string :logo_url
|
|
29
|
+
t.boolean :hide_logo, null: false, default: false
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# The subject line, editable beside the banner's words.
|
|
2
|
+
#
|
|
3
|
+
# Its OWN migration, and that is the point rather than an oversight. The subject
|
|
4
|
+
# column was first written into AddCopyToStudioEmailSettings, which a host had
|
|
5
|
+
# already run — so the column silently never appeared, and the manager offered a
|
|
6
|
+
# field backed by nothing. Editing an applied migration does not re-apply it; it
|
|
7
|
+
# only makes the file disagree with the database it claims to describe.
|
|
8
|
+
class AddSubjectToStudioEmailSettings < ActiveRecord::Migration[7.2]
|
|
9
|
+
def change
|
|
10
|
+
# Nullable, because nil means INHERIT the registry default — the same
|
|
11
|
+
# distinction every other copy column carries. A NOT NULL default would
|
|
12
|
+
# collapse "never set" into "set to empty".
|
|
13
|
+
add_column :studio_email_settings, :subject, :string
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/studio/version.rb
CHANGED
data/lib/studio.rb
CHANGED
|
@@ -570,6 +570,18 @@ module Studio
|
|
|
570
570
|
constraints: { key: /[a-z0-9_]+/ }
|
|
571
571
|
delete "admin/emails/:key", to: "studio/emails#destroy",
|
|
572
572
|
constraints: { key: /[a-z0-9_]+/ }
|
|
573
|
+
# Operator-tunable per-email settings (the banner scrim today). Separate
|
|
574
|
+
# from #update, which takes an image upload.
|
|
575
|
+
patch "admin/emails/:key/settings", to: "studio/emails#settings",
|
|
576
|
+
as: :admin_email_settings, constraints: { key: /[a-z0-9_]+/ }
|
|
577
|
+
# The banner's words and logo. Its own route so writing a sentence and
|
|
578
|
+
# nudging the tint save independently.
|
|
579
|
+
patch "admin/emails/:key/copy", to: "studio/emails#copy",
|
|
580
|
+
as: :admin_email_copy, constraints: { key: /[a-z0-9_]+/ }
|
|
581
|
+
# The per-email logo. Separate from the banner upload above — different
|
|
582
|
+
# picture, different ImageCache purpose, independently revertible.
|
|
583
|
+
patch "admin/emails/:key/logo", to: "studio/emails#logo",
|
|
584
|
+
as: :admin_email_logo, constraints: { key: /[a-z0-9_]+/ }
|
|
573
585
|
end
|
|
574
586
|
|
|
575
587
|
# DEPRECATED, kept for ONE release. Not a redirect: consumer-ci.yml runs
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: studio-engine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.42.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex McRitchie
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -169,7 +169,10 @@ files:
|
|
|
169
169
|
- LICENSE
|
|
170
170
|
- README.md
|
|
171
171
|
- app/assets/images/emails/email-change-confirmation.gif
|
|
172
|
+
- app/assets/images/emails/logo-horizontal.png
|
|
173
|
+
- app/assets/images/emails/magic-link-background.gif
|
|
172
174
|
- app/assets/images/emails/magic-link.gif
|
|
175
|
+
- app/assets/images/emails/newsletter-subscribed-background.gif
|
|
173
176
|
- app/assets/images/resend-favicon.png
|
|
174
177
|
- app/assets/images/ses-favicon.png
|
|
175
178
|
- app/assets/javascripts/studio/canvas_confetti.js
|
|
@@ -210,6 +213,7 @@ files:
|
|
|
210
213
|
- app/jobs/error_log_cleanup_job.rb
|
|
211
214
|
- app/jobs/studio/email_delivery_job.rb
|
|
212
215
|
- app/mailers/application_mailer.rb
|
|
216
|
+
- app/mailers/studio/newsletter_mailer.rb
|
|
213
217
|
- app/mailers/user_mailer.rb
|
|
214
218
|
- app/models/concerns/sluggable.rb
|
|
215
219
|
- app/models/concerns/studio/board/rankable.rb
|
|
@@ -219,13 +223,16 @@ files:
|
|
|
219
223
|
- app/models/image_cache.rb
|
|
220
224
|
- app/models/session_context.rb
|
|
221
225
|
- app/models/studio/email_delivery.rb
|
|
226
|
+
- app/models/studio/email_setting.rb
|
|
222
227
|
- app/models/studio/enumeral.rb
|
|
223
228
|
- app/models/studio/link.rb
|
|
224
229
|
- app/models/studio/model_page.rb
|
|
225
230
|
- app/models/theme_setting.rb
|
|
226
231
|
- app/services/google_oauth_validator.rb
|
|
232
|
+
- app/services/studio/banner.rb
|
|
227
233
|
- app/services/studio/email_catalog.rb
|
|
228
234
|
- app/services/studio/email_image.rb
|
|
235
|
+
- app/services/studio/email_preview_target.rb
|
|
229
236
|
- app/views/components/_admin_dropdown.html.erb
|
|
230
237
|
- app/views/components/_avatar.html.erb
|
|
231
238
|
- app/views/components/_avatar_cropper.html.erb
|
|
@@ -277,11 +284,17 @@ files:
|
|
|
277
284
|
- app/views/studio/board/_card_shell.html.erb
|
|
278
285
|
- app/views/studio/board/_column.html.erb
|
|
279
286
|
- app/views/studio/email_images/index.html.erb
|
|
287
|
+
- app/views/studio/emails/_banner_editor.html.erb
|
|
288
|
+
- app/views/studio/emails/_banner_scale.html.erb
|
|
289
|
+
- app/views/studio/emails/_recipient_picker.html.erb
|
|
290
|
+
- app/views/studio/emails/_recipient_repaint.html.erb
|
|
280
291
|
- app/views/studio/emails/_row.html.erb
|
|
281
292
|
- app/views/studio/emails/index.html.erb
|
|
293
|
+
- app/views/studio/emails/orphan.html.erb
|
|
282
294
|
- app/views/studio/emails/show.html.erb
|
|
283
295
|
- app/views/studio/links/confirm.html.erb
|
|
284
296
|
- app/views/studio/local_emails/index.html.erb
|
|
297
|
+
- app/views/studio/mailers/_layered_banner.html.erb
|
|
285
298
|
- app/views/studio/modals/_crop_photo.html.erb
|
|
286
299
|
- app/views/studio/modals/_host.html.erb
|
|
287
300
|
- app/views/studio/modals/_image_upload.html.erb
|
|
@@ -315,6 +328,8 @@ files:
|
|
|
315
328
|
- app/views/studio/modals/templates/_success.html.erb
|
|
316
329
|
- app/views/studio/modals/templates/_wizard.html.erb
|
|
317
330
|
- app/views/studio/models/show.html.erb
|
|
331
|
+
- app/views/studio/newsletter_mailer/subscribed.html.erb
|
|
332
|
+
- app/views/studio/newsletter_mailer/subscribed.text.erb
|
|
318
333
|
- app/views/style/_modal_specimen.html.erb
|
|
319
334
|
- app/views/style/_modals.html.erb
|
|
320
335
|
- app/views/style/_specimen.html.erb
|
|
@@ -336,6 +351,9 @@ files:
|
|
|
336
351
|
- db/migrate/20260620000001_create_studio_links.rb
|
|
337
352
|
- db/migrate/20260620000002_allow_null_image_cache_owner.rb
|
|
338
353
|
- db/migrate/20260623130000_create_studio_enumerals.rb
|
|
354
|
+
- db/migrate/20260812000000_create_studio_email_settings.rb
|
|
355
|
+
- db/migrate/20260812210000_add_copy_to_studio_email_settings.rb
|
|
356
|
+
- db/migrate/20260812220000_add_subject_to_studio_email_settings.rb
|
|
339
357
|
- lib/studio-engine.rb
|
|
340
358
|
- lib/studio.rb
|
|
341
359
|
- lib/studio/cable.rb
|