add admin email settings page with provider selection
All checks were successful
deploy / deploy (push) Successful in 56s

Card radio component for picking email providers (SMTP, SendGrid, Mailjet, etc.)
with instant client-side switching via JS hook. Adapter configs are pre-rendered
and toggled without a server round-trip. Secrets are preserved when re-saving
with blank password fields. Includes from address field, test email sending,
and disconnect flow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-02-21 19:29:34 +00:00
parent a2e46664c6
commit 366a1e6a48
17 changed files with 1176 additions and 39 deletions

View File

@@ -818,15 +818,21 @@
gap: 0.25rem;
}
/* Provider picker grid */
.setup-provider-grid {
/* Card radio group — selectable cards backed by radio inputs */
.card-radio-fieldset {
border: none;
padding: 0;
margin: 0;
}
.card-radio-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.5rem;
margin-bottom: 1rem;
margin-top: 0.5rem;
}
.setup-provider-card {
.card-radio-card {
display: flex;
flex-direction: column;
align-items: flex-start;
@@ -834,36 +840,48 @@
padding: 0.75rem;
border: 1px solid var(--t-border-default, #d4d4d4);
border-radius: 0.375rem;
text-align: left;
cursor: pointer;
transition: border-color 150ms, background 150ms;
-webkit-tap-highlight-color: transparent;
&:hover:not(:disabled) {
@media (hover: hover) {
&:hover:not(:has(:disabled)) {
border-color: color-mix(in oklch, var(--t-text-primary) 40%, transparent);
}
}
&:active:not(:has(:disabled)) {
background: color-mix(in oklch, var(--t-surface-sunken) 50%, transparent);
}
&.card-radio-card-selected {
border-color: var(--t-text-primary, #171717);
background: var(--t-surface-sunken, #e5e5e5);
}
}
.setup-provider-card-selected {
border-color: var(--t-text-primary, #171717);
background: var(--t-surface-sunken, #e5e5e5);
}
.setup-provider-card-disabled {
.card-radio-card-disabled {
opacity: 0.5;
cursor: not-allowed;
}
.setup-provider-name {
.card-radio-input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.card-radio-name {
font-size: 0.875rem;
font-weight: 600;
}
.setup-provider-tagline {
.card-radio-description {
font-size: 0.75rem;
color: color-mix(in oklch, var(--t-text-primary) 60%, transparent);
}
.setup-provider-badge {
.card-radio-badge {
font-size: 0.6875rem;
padding: 0.125rem 0.375rem;
border-radius: 9999px;
@@ -871,6 +889,34 @@
color: color-mix(in oklch, var(--t-text-primary) 60%, transparent);
}
.card-radio-link {
font-size: 0.6875rem;
color: color-mix(in oklch, var(--t-text-primary) 60%, transparent);
text-decoration: underline;
text-underline-offset: 2px;
&:hover {
color: var(--t-text-primary, #171717);
}
}
/* Tags display mode */
.card-radio-tags {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
margin-top: 0.125rem;
}
.card-radio-tag {
font-size: 0.625rem;
padding: 0.0625rem 0.375rem;
border-radius: 9999px;
background: var(--t-surface-sunken, #e5e5e5);
color: color-mix(in oklch, var(--t-text-primary) 70%, transparent);
white-space: nowrap;
}
.setup-provider-form {
margin-top: 0.75rem;
}

View File

@@ -487,10 +487,32 @@ const CollectionFilters = {
}
}
const CardRadioScroll = {
mounted() {
this.el.addEventListener("change", (e) => {
if (!e.target.matches('input[type="radio"]')) return
const key = e.target.value
const form = this.el.closest("form")
if (!form) return
form.querySelectorAll("[data-adapter]").forEach((section) => {
const match = section.dataset.adapter === key
section.hidden = !match
section.querySelectorAll("input, textarea, select, button[type='submit']").forEach((input) => {
input.disabled = !match
})
})
const target = document.getElementById(`adapter-config-${key}`)
if (target) target.scrollIntoView({ behavior: "smooth", block: "nearest" })
})
}
}
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
const liveSocket = new LiveSocket("/live", Socket, {
params: {_csrf_token: csrfToken},
hooks: {...colocatedHooks, ColorSync, Lightbox, CartPersist, CartDrawer, ProductImageScroll, SearchModal, CollectionFilters},
hooks: {...colocatedHooks, ColorSync, Lightbox, CartPersist, CartDrawer, ProductImageScroll, SearchModal, CollectionFilters, CardRadioScroll},
})
// Show progress bar on live navigation and form submits