forked from tsai/budibase
committed by
GitHub
47 changed files with 2593 additions and 2210 deletions
@ -0,0 +1,22 @@ |
|||||
|
<script> |
||||
|
import { |
||||
|
Body, |
||||
|
Menu, |
||||
|
MenuItem, |
||||
|
Detail, |
||||
|
MenuSection, |
||||
|
DetailSummary, |
||||
|
} from "@budibase/bbui" |
||||
|
|
||||
|
export let bindings |
||||
|
export let onBindingClick = () => {} |
||||
|
</script> |
||||
|
|
||||
|
<Menu> |
||||
|
{#each bindings as binding} |
||||
|
<MenuItem on:click={() => onBindingClick(binding)}> |
||||
|
<Detail size="M">{binding.name}</Detail> |
||||
|
<Body size="XS" noPadding>{binding.description}</Body> |
||||
|
</MenuItem> |
||||
|
{/each} |
||||
|
</Menu> |
||||
@ -0,0 +1,13 @@ |
|||||
|
<script> |
||||
|
import { goto } from "@roxi/routify" |
||||
|
|
||||
|
export let value |
||||
|
</script> |
||||
|
|
||||
|
<span on:click={() => $goto(`./${value}`)}>{value}</span> |
||||
|
|
||||
|
<style> |
||||
|
span { |
||||
|
text-transform: capitalize; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,142 @@ |
|||||
|
<script> |
||||
|
import { |
||||
|
Menu, |
||||
|
MenuItem, |
||||
|
Button, |
||||
|
Detail, |
||||
|
Heading, |
||||
|
Divider, |
||||
|
Label, |
||||
|
Modal, |
||||
|
ModalContent, |
||||
|
notifications, |
||||
|
Layout, |
||||
|
Icon, |
||||
|
Body, |
||||
|
Page, |
||||
|
Select, |
||||
|
Tabs, |
||||
|
Tab, |
||||
|
MenuSection, |
||||
|
MenuSeparator, |
||||
|
} from "@budibase/bbui" |
||||
|
import { goto } from "@roxi/routify" |
||||
|
import { onMount } from "svelte" |
||||
|
import { fade } from "svelte/transition" |
||||
|
import { email } from "stores/portal" |
||||
|
import Editor from "components/integration/QueryEditor.svelte" |
||||
|
import TemplateBindings from "./TemplateBindings.svelte" |
||||
|
import api from "builderStore/api" |
||||
|
|
||||
|
const ConfigTypes = { |
||||
|
SMTP: "smtp", |
||||
|
} |
||||
|
|
||||
|
export let template |
||||
|
|
||||
|
let selected = "Edit" |
||||
|
let selectedBindingTab = "Template" |
||||
|
let htmlEditor |
||||
|
|
||||
|
$: selectedTemplate = $email.templates.find( |
||||
|
({ purpose }) => purpose === template |
||||
|
) |
||||
|
$: templateBindings = |
||||
|
$email.definitions?.bindings[selectedTemplate.purpose] || [] |
||||
|
|
||||
|
async function saveTemplate() { |
||||
|
try { |
||||
|
// Save your template config |
||||
|
await email.templates.save(selectedTemplate) |
||||
|
notifications.success(`Template saved.`) |
||||
|
} catch (err) { |
||||
|
notifications.error(`Failed to update template settings. ${err}`) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function setTemplateBinding(binding) { |
||||
|
htmlEditor.update((selectedTemplate.contents += `{{ ${binding.name} }}`)) |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<Page wide gap="L"> |
||||
|
<div class="backbutton" on:click={() => $goto("./")}> |
||||
|
<Icon name="BackAndroid" /> |
||||
|
<span>Back</span> |
||||
|
</div> |
||||
|
<header> |
||||
|
<Heading> |
||||
|
Email Template: {template} |
||||
|
</Heading> |
||||
|
<Button cta on:click={saveTemplate}>Save</Button> |
||||
|
</header> |
||||
|
<Tabs {selected}> |
||||
|
<Tab title="Edit"> |
||||
|
<div class="template-editor"> |
||||
|
<Editor |
||||
|
editorHeight={800} |
||||
|
bind:this={htmlEditor} |
||||
|
mode="handlebars" |
||||
|
on:change={e => { |
||||
|
selectedTemplate.contents = e.detail.value |
||||
|
}} |
||||
|
value={selectedTemplate.contents} |
||||
|
/> |
||||
|
<div class="bindings-editor"> |
||||
|
<Detail size="L">Bindings</Detail> |
||||
|
<Tabs selected={selectedBindingTab}> |
||||
|
<Tab title="Template"> |
||||
|
<TemplateBindings |
||||
|
title="Template Bindings" |
||||
|
bindings={templateBindings} |
||||
|
onBindingClick={setTemplateBinding} |
||||
|
/> |
||||
|
</Tab> |
||||
|
<Tab title="Common"> |
||||
|
<TemplateBindings |
||||
|
title="Common Bindings" |
||||
|
bindings={$email.definitions.bindings.common} |
||||
|
onBindingClick={setTemplateBinding} |
||||
|
/> |
||||
|
</Tab> |
||||
|
</Tabs> |
||||
|
</div> |
||||
|
</div></Tab |
||||
|
> |
||||
|
<Tab title="Preview"> |
||||
|
<div class="preview" transition:fade> |
||||
|
{@html selectedTemplate.contents} |
||||
|
</div> |
||||
|
</Tab> |
||||
|
</Tabs> |
||||
|
</Page> |
||||
|
|
||||
|
<style> |
||||
|
.template-editor { |
||||
|
display: grid; |
||||
|
grid-template-columns: 1fr 20%; |
||||
|
grid-gap: var(--spacing-xl); |
||||
|
margin-top: var(--spacing-xl); |
||||
|
} |
||||
|
|
||||
|
header { |
||||
|
display: flex; |
||||
|
width: 100%; |
||||
|
justify-content: space-between; |
||||
|
margin-bottom: var(--spacing-l); |
||||
|
margin-top: var(--spacing-l); |
||||
|
} |
||||
|
|
||||
|
.preview { |
||||
|
background: white; |
||||
|
height: 800px; |
||||
|
padding: var(--spacing-xl); |
||||
|
} |
||||
|
|
||||
|
.backbutton { |
||||
|
display: flex; |
||||
|
gap: var(--spacing-m); |
||||
|
margin-bottom: var(--spacing-xl); |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,191 @@ |
|||||
|
<script> |
||||
|
import { goto } from "@roxi/routify" |
||||
|
import { |
||||
|
Menu, |
||||
|
MenuItem, |
||||
|
Button, |
||||
|
Heading, |
||||
|
Divider, |
||||
|
Label, |
||||
|
Modal, |
||||
|
ModalContent, |
||||
|
notifications, |
||||
|
Layout, |
||||
|
Input, |
||||
|
TextArea, |
||||
|
Body, |
||||
|
Page, |
||||
|
Select, |
||||
|
MenuSection, |
||||
|
MenuSeparator, |
||||
|
Table, |
||||
|
} from "@budibase/bbui" |
||||
|
import { onMount } from "svelte" |
||||
|
import { email } from "stores/portal" |
||||
|
import Editor from "components/integration/QueryEditor.svelte" |
||||
|
import TemplateBindings from "./TemplateBindings.svelte" |
||||
|
import TemplateLink from "./TemplateLink.svelte" |
||||
|
import api from "builderStore/api" |
||||
|
|
||||
|
const ConfigTypes = { |
||||
|
SMTP: "smtp", |
||||
|
} |
||||
|
|
||||
|
const templateSchema = { |
||||
|
purpose: { |
||||
|
displayName: "Email", |
||||
|
editable: false, |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
const customRenderers = [ |
||||
|
{ |
||||
|
column: "purpose", |
||||
|
component: TemplateLink, |
||||
|
}, |
||||
|
] |
||||
|
|
||||
|
let smtpConfig |
||||
|
let bindingsOpen = false |
||||
|
let htmlModal |
||||
|
let htmlEditor |
||||
|
let loading |
||||
|
|
||||
|
async function saveSmtp() { |
||||
|
try { |
||||
|
// Save your SMTP config |
||||
|
const response = await api.post(`/api/admin/configs`, smtpConfig) |
||||
|
const json = await response.json() |
||||
|
if (response.status !== 200) throw new Error(json.message) |
||||
|
smtpConfig._rev = json._rev |
||||
|
smtpConfig._id = json._id |
||||
|
|
||||
|
notifications.success(`Settings saved.`) |
||||
|
} catch (err) { |
||||
|
notifications.error(`Failed to save email settings. ${err}`) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function saveTemplate() { |
||||
|
try { |
||||
|
await email.templates.save(selectedTemplate) |
||||
|
notifications.success(`Template saved.`) |
||||
|
} catch (err) { |
||||
|
notifications.error(`Failed to update template settings. ${err}`) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function fetchSmtp() { |
||||
|
// fetch the configs for smtp |
||||
|
const smtpResponse = await api.get(`/api/admin/configs/${ConfigTypes.SMTP}`) |
||||
|
const smtpDoc = await smtpResponse.json() |
||||
|
|
||||
|
if (!smtpDoc._id) { |
||||
|
smtpConfig = { |
||||
|
type: ConfigTypes.SMTP, |
||||
|
config: { |
||||
|
auth: { |
||||
|
type: "login", |
||||
|
}, |
||||
|
}, |
||||
|
} |
||||
|
} else { |
||||
|
smtpConfig = smtpDoc |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
onMount(async () => { |
||||
|
loading = true |
||||
|
await fetchSmtp() |
||||
|
await email.templates.fetch() |
||||
|
loading = false |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<Page> |
||||
|
<header> |
||||
|
<Heading size="M">Email</Heading> |
||||
|
<Body size="S"> |
||||
|
Sending email is not required, but highly recommended for processes such |
||||
|
as password recovery. To setup automated auth emails, simply add the |
||||
|
values below and click activate. |
||||
|
</Body> |
||||
|
</header> |
||||
|
<Divider /> |
||||
|
{#if smtpConfig} |
||||
|
<div class="config-form"> |
||||
|
<Heading size="S">SMTP</Heading> |
||||
|
<Body size="S"> |
||||
|
To allow your app to benefit from automated auth emails, add your SMTP |
||||
|
details below. |
||||
|
</Body> |
||||
|
<Layout gap="S"> |
||||
|
<Heading size="S"> |
||||
|
<span /> |
||||
|
</Heading> |
||||
|
<div class="form-row"> |
||||
|
<Label>Host</Label> |
||||
|
<Input bind:value={smtpConfig.config.host} /> |
||||
|
</div> |
||||
|
<div class="form-row"> |
||||
|
<Label>Port</Label> |
||||
|
<Input type="number" bind:value={smtpConfig.config.port} /> |
||||
|
</div> |
||||
|
<div class="form-row"> |
||||
|
<Label>User</Label> |
||||
|
<Input bind:value={smtpConfig.config.auth.user} /> |
||||
|
</div> |
||||
|
<div class="form-row"> |
||||
|
<Label>Password</Label> |
||||
|
<Input type="password" bind:value={smtpConfig.config.auth.pass} /> |
||||
|
</div> |
||||
|
<div class="form-row"> |
||||
|
<Label>From email address</Label> |
||||
|
<Input type="email" bind:value={smtpConfig.config.from} /> |
||||
|
</div> |
||||
|
</Layout> |
||||
|
<Button cta on:click={saveSmtp}>Save</Button> |
||||
|
</div> |
||||
|
<Divider /> |
||||
|
|
||||
|
<div class="config-form"> |
||||
|
<Heading size="S">Templates</Heading> |
||||
|
<Body size="S"> |
||||
|
Budibase comes out of the box with ready-made email templates to help |
||||
|
with user onboarding. Please refrain from changing the links. |
||||
|
</Body> |
||||
|
</div> |
||||
|
<Table |
||||
|
{customRenderers} |
||||
|
data={$email.templates} |
||||
|
schema={templateSchema} |
||||
|
{loading} |
||||
|
allowEditRows={false} |
||||
|
allowSelectRows={false} |
||||
|
allowEditColumns={false} |
||||
|
/> |
||||
|
{/if} |
||||
|
</Page> |
||||
|
|
||||
|
<style> |
||||
|
.config-form { |
||||
|
margin-top: 42px; |
||||
|
margin-bottom: 42px; |
||||
|
} |
||||
|
|
||||
|
.form-row { |
||||
|
display: grid; |
||||
|
grid-template-columns: 20% 1fr; |
||||
|
grid-gap: var(--spacing-l); |
||||
|
} |
||||
|
|
||||
|
span { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: var(--spacing-s); |
||||
|
} |
||||
|
|
||||
|
header { |
||||
|
margin-bottom: 42px; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,44 @@ |
|||||
|
import { writable } from "svelte/store" |
||||
|
import api from "builderStore/api" |
||||
|
|
||||
|
export function createEmailStore() { |
||||
|
const store = writable([]) |
||||
|
|
||||
|
return { |
||||
|
subscribe: store.subscribe, |
||||
|
templates: { |
||||
|
fetch: async () => { |
||||
|
// fetch the email template definitions
|
||||
|
const response = await api.get(`/api/admin/template/definitions`) |
||||
|
const definitions = await response.json() |
||||
|
|
||||
|
// fetch the email templates themselves
|
||||
|
const templatesResponse = await api.get(`/api/admin/template/email`) |
||||
|
const templates = await templatesResponse.json() |
||||
|
|
||||
|
store.set({ |
||||
|
definitions, |
||||
|
templates, |
||||
|
}) |
||||
|
}, |
||||
|
save: async template => { |
||||
|
// Save your template config
|
||||
|
const response = await api.post(`/api/admin/template`, template) |
||||
|
const json = await response.json() |
||||
|
if (response.status !== 200) throw new Error(json.message) |
||||
|
template._rev = json._rev |
||||
|
template._id = json._id |
||||
|
|
||||
|
store.update(state => { |
||||
|
const currentIdx = state.templates.findIndex( |
||||
|
template => template.purpose === json.purpose |
||||
|
) |
||||
|
state.templates.splice(currentIdx, 1, template) |
||||
|
return state |
||||
|
}) |
||||
|
}, |
||||
|
}, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export const email = createEmailStore() |
||||
@ -1,3 +1,4 @@ |
|||||
export { organisation } from "./organisation" |
export { organisation } from "./organisation" |
||||
export { admin } from "./admin" |
export { admin } from "./admin" |
||||
export { apps } from "./apps" |
export { apps } from "./apps" |
||||
|
export { email } from "./email" |
||||
|
|||||
File diff suppressed because it is too large
@ -0,0 +1,67 @@ |
|||||
|
const { sendSmtpEmail } = require("../../utilities/workerRequests") |
||||
|
|
||||
|
module.exports.definition = { |
||||
|
description: "Send an email using SMTP", |
||||
|
tagline: "Send SMTP email to {{inputs.to}}", |
||||
|
icon: "ri-mail-open-line", |
||||
|
name: "Send Email (SMTP)", |
||||
|
type: "ACTION", |
||||
|
stepId: "SEND_EMAIL_SMTP", |
||||
|
inputs: {}, |
||||
|
schema: { |
||||
|
inputs: { |
||||
|
properties: { |
||||
|
to: { |
||||
|
type: "string", |
||||
|
title: "Send To", |
||||
|
}, |
||||
|
from: { |
||||
|
type: "string", |
||||
|
title: "Send From", |
||||
|
}, |
||||
|
subject: { |
||||
|
type: "string", |
||||
|
title: "Email Subject", |
||||
|
}, |
||||
|
contents: { |
||||
|
type: "string", |
||||
|
title: "HTML Contents", |
||||
|
}, |
||||
|
}, |
||||
|
required: ["to", "from", "subject", "contents"], |
||||
|
}, |
||||
|
outputs: { |
||||
|
properties: { |
||||
|
success: { |
||||
|
type: "boolean", |
||||
|
description: "Whether the email was sent", |
||||
|
}, |
||||
|
response: { |
||||
|
type: "object", |
||||
|
description: "A response from the email client, this may be an error", |
||||
|
}, |
||||
|
}, |
||||
|
required: ["success"], |
||||
|
}, |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
module.exports.run = async function ({ inputs }) { |
||||
|
let { to, from, subject, contents } = inputs |
||||
|
if (!contents) { |
||||
|
contents = "<h1>No content</h1>" |
||||
|
} |
||||
|
try { |
||||
|
let response = await sendSmtpEmail(to, from, subject, contents) |
||||
|
return { |
||||
|
success: true, |
||||
|
response, |
||||
|
} |
||||
|
} catch (err) { |
||||
|
console.error(err) |
||||
|
return { |
||||
|
success: false, |
||||
|
response: err, |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,8 +1,8 @@ |
|||||
module.exports.definition = { |
module.exports.definition = { |
||||
description: "Send an email", |
description: "Send an email using SendGrid", |
||||
tagline: "Send email to {{inputs.to}}", |
tagline: "Send email to {{inputs.to}}", |
||||
icon: "ri-mail-open-line", |
icon: "ri-mail-open-line", |
||||
name: "Send Email", |
name: "Send Email (SendGrid)", |
||||
type: "ACTION", |
type: "ACTION", |
||||
stepId: "SEND_EMAIL", |
stepId: "SEND_EMAIL", |
||||
inputs: {}, |
inputs: {}, |
||||
File diff suppressed because it is too large
@ -0,0 +1,93 @@ |
|||||
|
const authPkg = require("@budibase/auth") |
||||
|
const { google } = require("@budibase/auth/src/middleware") |
||||
|
const { Configs } = require("../../constants") |
||||
|
const CouchDB = require("../../db") |
||||
|
const { clearCookie } = authPkg.utils |
||||
|
const { Cookies } = authPkg.constants |
||||
|
const { passport } = authPkg.auth |
||||
|
|
||||
|
const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name |
||||
|
|
||||
|
exports.authenticate = async (ctx, next) => { |
||||
|
return passport.authenticate("local", async (err, user) => { |
||||
|
if (err) { |
||||
|
return ctx.throw(403, "Unauthorized") |
||||
|
} |
||||
|
|
||||
|
const expires = new Date() |
||||
|
expires.setDate(expires.getDate() + 1) |
||||
|
|
||||
|
if (!user) { |
||||
|
return ctx.throw(403, "Unauthorized") |
||||
|
} |
||||
|
|
||||
|
ctx.cookies.set(Cookies.Auth, user.token, { |
||||
|
expires, |
||||
|
path: "/", |
||||
|
httpOnly: false, |
||||
|
overwrite: true, |
||||
|
}) |
||||
|
|
||||
|
delete user.token |
||||
|
|
||||
|
ctx.body = { user } |
||||
|
})(ctx, next) |
||||
|
} |
||||
|
|
||||
|
exports.logout = async ctx => { |
||||
|
clearCookie(ctx, Cookies.Auth) |
||||
|
ctx.body = { message: "User logged out" } |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* The initial call that google authentication makes to take you to the google login screen. |
||||
|
* On a successful login, you will be redirected to the googleAuth callback route. |
||||
|
*/ |
||||
|
exports.googlePreAuth = async (ctx, next) => { |
||||
|
const db = new CouchDB(GLOBAL_DB) |
||||
|
const config = await authPkg.db.getScopedFullConfig(db, { |
||||
|
type: Configs.GOOGLE, |
||||
|
group: ctx.query.group, |
||||
|
}) |
||||
|
const strategy = await google.strategyFactory(config) |
||||
|
|
||||
|
return passport.authenticate(strategy, { |
||||
|
scope: ["profile", "email"], |
||||
|
})(ctx, next) |
||||
|
} |
||||
|
|
||||
|
exports.googleAuth = async (ctx, next) => { |
||||
|
const db = new CouchDB(GLOBAL_DB) |
||||
|
|
||||
|
const config = await authPkg.db.getScopedFullConfig(db, { |
||||
|
type: Configs.GOOGLE, |
||||
|
group: ctx.query.group, |
||||
|
}) |
||||
|
const strategy = await google.strategyFactory(config) |
||||
|
|
||||
|
return passport.authenticate( |
||||
|
strategy, |
||||
|
{ successRedirect: "/", failureRedirect: "/error" }, |
||||
|
async (err, user) => { |
||||
|
if (err) { |
||||
|
return ctx.throw(403, "Unauthorized") |
||||
|
} |
||||
|
|
||||
|
const expires = new Date() |
||||
|
expires.setDate(expires.getDate() + 1) |
||||
|
|
||||
|
if (!user) { |
||||
|
return ctx.throw(403, "Unauthorized") |
||||
|
} |
||||
|
|
||||
|
ctx.cookies.set(Cookies.Auth, user.token, { |
||||
|
expires, |
||||
|
path: "/", |
||||
|
httpOnly: false, |
||||
|
overwrite: true, |
||||
|
}) |
||||
|
|
||||
|
ctx.redirect("/") |
||||
|
} |
||||
|
)(ctx, next) |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
<tr> |
||||
|
<td class="email-body" width="570" cellpadding="0" cellspacing="0"> |
||||
|
<table class="email-body_inner" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation"> |
||||
|
<!-- Body content --> |
||||
|
<tr> |
||||
|
<td class="content-cell"> |
||||
|
<div class="f-fallback"> |
||||
|
{{ contents }} |
||||
|
</div> |
||||
|
</td> |
||||
|
</tr> |
||||
|
</table> |
||||
|
</td> |
||||
|
</tr> |
||||
@ -1,408 +0,0 @@ |
|||||
/* Based on templates: https://github.com/wildbit/postmark-templates/blob/master/templates/plain */ |
|
||||
/* Base ------------------------------ */ |
|
||||
|
|
||||
@import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro&display=swap'); |
|
||||
body { |
|
||||
width: 100% !important; |
|
||||
height: 100%; |
|
||||
margin: 0; |
|
||||
-webkit-text-size-adjust: none; |
|
||||
} |
|
||||
|
|
||||
a { |
|
||||
color: #3869D4; |
|
||||
} |
|
||||
|
|
||||
a img { |
|
||||
border: none; |
|
||||
} |
|
||||
|
|
||||
td { |
|
||||
word-break: break-word; |
|
||||
} |
|
||||
|
|
||||
.preheader { |
|
||||
display: none !important; |
|
||||
visibility: hidden; |
|
||||
mso-hide: all; |
|
||||
font-size: 1px; |
|
||||
line-height: 1px; |
|
||||
max-height: 0; |
|
||||
max-width: 0; |
|
||||
opacity: 0; |
|
||||
overflow: hidden; |
|
||||
} |
|
||||
/* Type ------------------------------ */ |
|
||||
|
|
||||
body, |
|
||||
td, |
|
||||
th { |
|
||||
font-family: "Source Sans Pro", Helvetica, Arial, sans-serif; |
|
||||
} |
|
||||
|
|
||||
h1 { |
|
||||
margin-top: 0; |
|
||||
color: #333333; |
|
||||
font-size: 22px; |
|
||||
font-weight: bold; |
|
||||
text-align: left; |
|
||||
} |
|
||||
|
|
||||
h2 { |
|
||||
margin-top: 0; |
|
||||
color: #333333; |
|
||||
font-size: 16px; |
|
||||
font-weight: bold; |
|
||||
text-align: left; |
|
||||
} |
|
||||
|
|
||||
h3 { |
|
||||
margin-top: 0; |
|
||||
color: #333333; |
|
||||
font-size: 14px; |
|
||||
font-weight: bold; |
|
||||
text-align: left; |
|
||||
} |
|
||||
|
|
||||
td, |
|
||||
th { |
|
||||
font-size: 16px; |
|
||||
} |
|
||||
|
|
||||
p, |
|
||||
ul, |
|
||||
ol, |
|
||||
blockquote { |
|
||||
margin: .4em 0 1.1875em; |
|
||||
font-size: 16px; |
|
||||
line-height: 1.625; |
|
||||
} |
|
||||
|
|
||||
p.sub { |
|
||||
font-size: 13px; |
|
||||
} |
|
||||
/* Utilities ------------------------------ */ |
|
||||
|
|
||||
.align-right { |
|
||||
text-align: right; |
|
||||
} |
|
||||
|
|
||||
.align-left { |
|
||||
text-align: left; |
|
||||
} |
|
||||
|
|
||||
.align-center { |
|
||||
text-align: center; |
|
||||
} |
|
||||
/* Buttons ------------------------------ */ |
|
||||
|
|
||||
.button { |
|
||||
background-color: #3869D4; |
|
||||
border-top: 10px solid #3869D4; |
|
||||
border-right: 18px solid #3869D4; |
|
||||
border-bottom: 10px solid #3869D4; |
|
||||
border-left: 18px solid #3869D4; |
|
||||
display: inline-block; |
|
||||
color: #FFF; |
|
||||
text-decoration: none; |
|
||||
border-radius: 3px; |
|
||||
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16); |
|
||||
-webkit-text-size-adjust: none; |
|
||||
box-sizing: border-box; |
|
||||
} |
|
||||
|
|
||||
.button--green { |
|
||||
background-color: #22BC66; |
|
||||
border-top: 10px solid #22BC66; |
|
||||
border-right: 18px solid #22BC66; |
|
||||
border-bottom: 10px solid #22BC66; |
|
||||
border-left: 18px solid #22BC66; |
|
||||
} |
|
||||
|
|
||||
.button--red { |
|
||||
background-color: #FF6136; |
|
||||
border-top: 10px solid #FF6136; |
|
||||
border-right: 18px solid #FF6136; |
|
||||
border-bottom: 10px solid #FF6136; |
|
||||
border-left: 18px solid #FF6136; |
|
||||
} |
|
||||
|
|
||||
@media only screen and (max-width: 500px) { |
|
||||
.button { |
|
||||
width: 100% !important; |
|
||||
text-align: center !important; |
|
||||
} |
|
||||
} |
|
||||
/* Attribute list ------------------------------ */ |
|
||||
|
|
||||
.attributes { |
|
||||
margin: 0 0 21px; |
|
||||
} |
|
||||
|
|
||||
.attributes_content { |
|
||||
background-color: #F4F4F7; |
|
||||
padding: 16px; |
|
||||
} |
|
||||
|
|
||||
.attributes_item { |
|
||||
padding: 0; |
|
||||
} |
|
||||
/* Related Items ------------------------------ */ |
|
||||
|
|
||||
.related { |
|
||||
width: 100%; |
|
||||
margin: 0; |
|
||||
padding: 25px 0 0 0; |
|
||||
-premailer-width: 100%; |
|
||||
-premailer-cellpadding: 0; |
|
||||
-premailer-cellspacing: 0; |
|
||||
} |
|
||||
|
|
||||
.related_item { |
|
||||
padding: 10px 0; |
|
||||
color: #CBCCCF; |
|
||||
font-size: 15px; |
|
||||
line-height: 18px; |
|
||||
} |
|
||||
|
|
||||
.related_item-title { |
|
||||
display: block; |
|
||||
margin: .5em 0 0; |
|
||||
} |
|
||||
|
|
||||
.related_item-thumb { |
|
||||
display: block; |
|
||||
padding-bottom: 10px; |
|
||||
} |
|
||||
|
|
||||
.related_heading { |
|
||||
border-top: 1px solid #CBCCCF; |
|
||||
text-align: center; |
|
||||
padding: 25px 0 10px; |
|
||||
} |
|
||||
/* Discount Code ------------------------------ */ |
|
||||
|
|
||||
.discount { |
|
||||
width: 100%; |
|
||||
margin: 0; |
|
||||
padding: 24px; |
|
||||
-premailer-width: 100%; |
|
||||
-premailer-cellpadding: 0; |
|
||||
-premailer-cellspacing: 0; |
|
||||
background-color: #F4F4F7; |
|
||||
border: 2px dashed #CBCCCF; |
|
||||
} |
|
||||
|
|
||||
.discount_heading { |
|
||||
text-align: center; |
|
||||
} |
|
||||
|
|
||||
.discount_body { |
|
||||
text-align: center; |
|
||||
font-size: 15px; |
|
||||
} |
|
||||
/* Social Icons ------------------------------ */ |
|
||||
|
|
||||
.social { |
|
||||
width: auto; |
|
||||
} |
|
||||
|
|
||||
.social td { |
|
||||
padding: 0; |
|
||||
width: auto; |
|
||||
} |
|
||||
|
|
||||
.social_icon { |
|
||||
height: 20px; |
|
||||
margin: 0 8px 10px 8px; |
|
||||
padding: 0; |
|
||||
} |
|
||||
/* Data table ------------------------------ */ |
|
||||
|
|
||||
.purchase { |
|
||||
width: 100%; |
|
||||
margin: 0; |
|
||||
padding: 35px 0; |
|
||||
-premailer-width: 100%; |
|
||||
-premailer-cellpadding: 0; |
|
||||
-premailer-cellspacing: 0; |
|
||||
} |
|
||||
|
|
||||
.purchase_content { |
|
||||
width: 100%; |
|
||||
margin: 0; |
|
||||
padding: 25px 0 0 0; |
|
||||
-premailer-width: 100%; |
|
||||
-premailer-cellpadding: 0; |
|
||||
-premailer-cellspacing: 0; |
|
||||
} |
|
||||
|
|
||||
.purchase_item { |
|
||||
padding: 10px 0; |
|
||||
color: #51545E; |
|
||||
font-size: 15px; |
|
||||
line-height: 18px; |
|
||||
} |
|
||||
|
|
||||
.purchase_heading { |
|
||||
padding-bottom: 8px; |
|
||||
border-bottom: 1px solid #EAEAEC; |
|
||||
} |
|
||||
|
|
||||
.purchase_heading p { |
|
||||
margin: 0; |
|
||||
color: #85878E; |
|
||||
font-size: 12px; |
|
||||
} |
|
||||
|
|
||||
.purchase_footer { |
|
||||
padding-top: 15px; |
|
||||
border-top: 1px solid #EAEAEC; |
|
||||
} |
|
||||
|
|
||||
.purchase_total { |
|
||||
margin: 0; |
|
||||
text-align: right; |
|
||||
font-weight: bold; |
|
||||
color: #333333; |
|
||||
} |
|
||||
|
|
||||
.purchase_total--label { |
|
||||
padding: 0 15px 0 0; |
|
||||
} |
|
||||
|
|
||||
body { |
|
||||
background-color: #FFF; |
|
||||
color: #333; |
|
||||
} |
|
||||
|
|
||||
p { |
|
||||
color: #333; |
|
||||
} |
|
||||
|
|
||||
.email-wrapper { |
|
||||
width: 100%; |
|
||||
margin: 0; |
|
||||
padding: 0; |
|
||||
-premailer-width: 100%; |
|
||||
-premailer-cellpadding: 0; |
|
||||
-premailer-cellspacing: 0; |
|
||||
} |
|
||||
|
|
||||
.email-content { |
|
||||
width: 100%; |
|
||||
margin: 0; |
|
||||
padding: 0; |
|
||||
-premailer-width: 100%; |
|
||||
-premailer-cellpadding: 0; |
|
||||
-premailer-cellspacing: 0; |
|
||||
} |
|
||||
/* Masthead ----------------------- */ |
|
||||
|
|
||||
.email-masthead { |
|
||||
padding: 25px 0; |
|
||||
text-align: center; |
|
||||
} |
|
||||
|
|
||||
.email-masthead_logo { |
|
||||
width: 94px; |
|
||||
} |
|
||||
|
|
||||
.email-masthead_name { |
|
||||
font-size: 16px; |
|
||||
font-weight: bold; |
|
||||
color: #A8AAAF; |
|
||||
text-decoration: none; |
|
||||
text-shadow: 0 1px 0 white; |
|
||||
} |
|
||||
/* Body ------------------------------ */ |
|
||||
|
|
||||
.email-body { |
|
||||
width: 100%; |
|
||||
margin: 0; |
|
||||
padding: 0; |
|
||||
-premailer-width: 100%; |
|
||||
-premailer-cellpadding: 0; |
|
||||
-premailer-cellspacing: 0; |
|
||||
} |
|
||||
|
|
||||
.email-body_inner { |
|
||||
width: 570px; |
|
||||
margin: 0 auto; |
|
||||
padding: 0; |
|
||||
-premailer-width: 570px; |
|
||||
-premailer-cellpadding: 0; |
|
||||
-premailer-cellspacing: 0; |
|
||||
} |
|
||||
|
|
||||
.email-footer { |
|
||||
width: 570px; |
|
||||
margin: 0 auto; |
|
||||
padding: 0; |
|
||||
-premailer-width: 570px; |
|
||||
-premailer-cellpadding: 0; |
|
||||
-premailer-cellspacing: 0; |
|
||||
text-align: center; |
|
||||
} |
|
||||
|
|
||||
.email-footer p { |
|
||||
color: #A8AAAF; |
|
||||
} |
|
||||
|
|
||||
.body-action { |
|
||||
width: 100%; |
|
||||
margin: 30px auto; |
|
||||
padding: 0; |
|
||||
-premailer-width: 100%; |
|
||||
-premailer-cellpadding: 0; |
|
||||
-premailer-cellspacing: 0; |
|
||||
text-align: center; |
|
||||
} |
|
||||
|
|
||||
.body-sub { |
|
||||
margin-top: 25px; |
|
||||
padding-top: 25px; |
|
||||
border-top: 1px solid #EAEAEC; |
|
||||
} |
|
||||
|
|
||||
.content-cell { |
|
||||
padding: 35px; |
|
||||
} |
|
||||
/*Media Queries ------------------------------ */ |
|
||||
|
|
||||
@media only screen and (max-width: 600px) { |
|
||||
.email-body_inner, |
|
||||
.email-footer { |
|
||||
width: 100% !important; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@media (prefers-color-scheme: dark) { |
|
||||
body { |
|
||||
background-color: #333333 !important; |
|
||||
color: #FFF !important; |
|
||||
} |
|
||||
p, |
|
||||
ul, |
|
||||
ol, |
|
||||
blockquote, |
|
||||
h1, |
|
||||
h2, |
|
||||
h3, |
|
||||
span, |
|
||||
.purchase_item { |
|
||||
color: #FFF !important; |
|
||||
} |
|
||||
.attributes_content, |
|
||||
.discount { |
|
||||
background-color: #222 !important; |
|
||||
} |
|
||||
.email-masthead_name { |
|
||||
text-shadow: none !important; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
:root { |
|
||||
color-scheme: light dark; |
|
||||
supported-color-schemes: light dark; |
|
||||
} |
|
||||
Loading…
Reference in new issue