add admin email settings page with provider selection
All checks were successful
deploy / deploy (push) Successful in 56s
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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user