fix email settings: missing providers, a11y, no-JS support

show all 10 providers in three groups (popular, transactional,
advanced) with category headings. fix phx-change clobbering text
fields, async test email sending state, integer parse crash on
bad port. add keyboard focus on card radios, fieldset legend,
WCAG-compliant badge contrast, responsive grid. extract shared
save_config into Mailer, add no-JS controller fallback with
configured_adapter hidden field for adapter change detection.
remove CardRadioScroll JS hook (no longer needed).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-03-04 21:26:59 +00:00
parent dd659e4c61
commit dd20ea824f
9 changed files with 637 additions and 235 deletions

View File

@@ -1459,6 +1459,10 @@
grid-template-columns: repeat(2, 1fr);
gap: 0.5rem;
margin-top: 0.5rem;
@media (max-width: 30rem) {
grid-template-columns: 1fr;
}
}
.card-radio-card {
@@ -1483,6 +1487,11 @@
background: color-mix(in oklch, var(--t-surface-sunken) 50%, transparent);
}
&:has(:focus-visible) {
outline: 2px solid var(--t-accent, oklch(0.55 0.2 250));
outline-offset: 2px;
}
&.card-radio-card-selected {
border-color: var(--t-text-primary, #171717);
background: var(--t-surface-sunken, #e5e5e5);
@@ -4272,7 +4281,7 @@
}
.card-radio-recommended {
background: var(--admin-accent, oklch(0.65 0.2 145));
background: oklch(0.45 0.15 145);
color: white;
font-weight: 500;
}
@@ -4338,6 +4347,29 @@
color: var(--t-status-error, oklch(0.6 0.2 25));
}
/* ── Provider group headings ── */
.card-radio-group-heading {
font-size: 0.8125rem;
font-weight: 600;
color: var(--admin-text-primary);
margin: 1rem 0 0;
}
.card-radio-group-heading:first-of-type {
margin-top: 0.5rem;
}
.card-radio-group-desc {
font-size: 0.75rem;
color: var(--admin-text-muted);
margin: 0.125rem 0 0;
}
.card-radio-group-hint {
font-weight: 500;
}
/* ── Email adapter config ── */
.admin-adapter-config {
@@ -4345,10 +4377,6 @@
display: flex;
flex-direction: column;
gap: 1.5rem;
&[hidden] {
display: none;
}
}
/* ── Campaign form ── */

View File

@@ -507,28 +507,6 @@ 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" })
})
}
}
// Analytics export: reads the current period and filters from the DOM at click time
// so the download URL is always correct, even if clicked before the LiveView re-render.
const AnalyticsExport = {
@@ -704,7 +682,7 @@ const EditorKeyboard = {
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
const liveSocket = new LiveSocket("/live", Socket, {
params: {_csrf_token: csrfToken, screen_width: window.innerWidth},
hooks: {...colocatedHooks, ColorSync, Lightbox, CartPersist, CartDrawer, ProductImageScroll, SearchModal, CollectionFilters, CardRadioScroll, AnalyticsInit, AnalyticsExport, ChartTooltip, DirtyGuard, EditorKeyboard},
hooks: {...colocatedHooks, ColorSync, Lightbox, CartPersist, CartDrawer, ProductImageScroll, SearchModal, CollectionFilters, AnalyticsInit, AnalyticsExport, ChartTooltip, DirtyGuard, EditorKeyboard},
})
// Show progress bar on live navigation and form submits