源码快照 @ 2026-09-02 (source-only, 剔除非源码素材)

This commit is contained in:
agent-radar
2026-09-02 01:05:41 +08:00
commit 04bac1f6e2
2200 changed files with 489415 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
import { FeedbackEmail, type FeedbackEmailProps } from "../src/templates/feedback"
export default function FeedbackPreview(props: FeedbackEmailProps) {
return <FeedbackEmail {...props} />
}
FeedbackPreview.PreviewProps = {
name: "Jane Doe",
email: "jane@example.com",
message: "I tried to connect a worker from Settings, but the connection stayed pending after the token was accepted.",
source: "openwork-app",
entrypoint: "settings",
deployment: "desktop",
appVersion: "0.13.5",
openworkServerVersion: "0.13.5",
opencodeVersion: "1.4.9",
osName: "macOS",
osVersion: "15.4",
platform: "MacIntel",
diagnosticsSummary: "Source: openwork-app\nEntrypoint: settings\nDeployment: desktop\nApp version: 0.13.5\nOS: macOS 15.4\nPlatform: MacIntel",
submittedAt: "2026-05-11T18:30:00.000Z",
} satisfies FeedbackEmailProps
@@ -0,0 +1,16 @@
import {
OrganizationInviteEmail,
type OrganizationInviteEmailProps,
} from "../src/templates/organization-invite"
export default function OrganizationInvitePreview(props: OrganizationInviteEmailProps) {
return <OrganizationInviteEmail {...props} />
}
OrganizationInvitePreview.PreviewProps = {
inviteLink: "https://app.openworklabs.com/join-org?invite=invitation_preview",
invitedByName: "Ada Lovelace",
invitedByEmail: "ada@example.com",
organizationName: "OpenWork Preview",
role: "admin",
} satisfies OrganizationInviteEmailProps
+9
View File
@@ -0,0 +1,9 @@
import { PasswordResetEmail, type PasswordResetEmailProps } from "../src/templates/password-reset"
export default function PasswordResetPreview(props: PasswordResetEmailProps) {
return <PasswordResetEmail {...props} />
}
PasswordResetPreview.PreviewProps = {
resetLink: "https://app.openworklabs.com/api/auth/reset-password/example-token?callbackURL=https%3A%2F%2Fapp.openworklabs.com%2Freset-password",
} satisfies PasswordResetEmailProps
+9
View File
@@ -0,0 +1,9 @@
import { VerificationEmail, type VerificationEmailProps } from "../src/templates/verification"
export default function VerificationPreview(props: VerificationEmailProps) {
return <VerificationEmail {...props} />
}
VerificationPreview.PreviewProps = {
verificationCode: "123456",
} satisfies VerificationEmailProps
+42
View File
@@ -0,0 +1,42 @@
{
"name": "@openwork/email",
"private": true,
"type": "module",
"exports": {
".": {
"types": "./src/index.ts",
"development": "./src/index.ts",
"default": "./dist/index.js"
},
"./templates": {
"types": "./src/templates/index.ts",
"development": "./src/templates/index.ts",
"default": "./dist/templates/index.js"
}
},
"files": [
"dist",
"src",
"emails"
],
"scripts": {
"build": "tsup",
"dev": "email dev --dir emails"
},
"dependencies": {
"@react-email/components": "^1.0.12",
"@react-email/render": "^2.1.0",
"nodemailer": "^9.0.3",
"react": "catalog:",
"react-dom": "catalog:",
"react-email": "6.9.1",
"resend": "^6.12.2"
},
"devDependencies": {
"@react-email/ui": "6.9.1",
"@types/nodemailer": "^8.0.0",
"@types/react": "^19.2.14",
"tsup": "^8.5.0",
"typescript": "^5.6.3"
}
}
+21
View File
@@ -0,0 +1,21 @@
export {
EmailSendError,
sendEmail,
type EmailProvider,
type EmailSendConfig,
type SendEmailInput,
type SmtpEmailConfig,
} from "./send-email.js"
export { renderEmailHtml } from "./render-email.js"
export {
emailSubjects,
emailReplyTo,
renderEmailTemplate,
type EmailTemplate,
type EmailTemplateProps,
type DownloadLinkEmailProps,
type FeedbackEmailProps,
type OrganizationInviteEmailProps,
type PasswordResetEmailProps,
type VerificationEmailProps,
} from "./templates/index.js"
+9
View File
@@ -0,0 +1,9 @@
import { render } from "@react-email/render"
import { renderEmailTemplate, type EmailTemplate, type EmailTemplateProps } from "./templates/index.js"
export function renderEmailHtml<Template extends EmailTemplate>(
template: Template,
props: EmailTemplateProps[Template],
) {
return render(renderEmailTemplate(template, props))
}
+198
View File
@@ -0,0 +1,198 @@
import { render } from "@react-email/render"
import nodemailer from "nodemailer"
import { Resend } from "resend"
import { emailReplyTo, emailSubjects, type EmailTemplate, type EmailTemplateProps, renderEmailTemplate } from "./templates/index.js"
export type EmailProvider = "dev" | "resend" | "nodemailer"
export type SmtpEmailConfig = {
host?: string
port?: number
user?: string
pass?: string
secure?: boolean
}
export type EmailSendConfig = {
from?: string
resendApiKey?: string
smtp?: SmtpEmailConfig
devMode?: boolean
}
export class EmailSendError extends Error {
readonly reason: "email_not_configured" | "resend_rejected" | "resend_network" | "nodemailer_rejected"
readonly template: EmailTemplate
readonly recipient: string
readonly detail?: string
constructor(input: {
template: EmailTemplate
reason: EmailSendError["reason"]
recipient: string
detail?: string
}) {
super(`[${input.template}] email for ${input.recipient} failed: ${input.reason}${input.detail ? ` (${input.detail})` : ""}`)
this.name = "EmailSendError"
this.reason = input.reason
this.template = input.template
this.recipient = input.recipient
this.detail = input.detail
}
}
function emailNotConfigured(input: {
template: EmailTemplate
recipient: string
detail: string
}) {
return new EmailSendError({
template: input.template,
reason: "email_not_configured",
recipient: input.recipient,
detail: input.detail,
})
}
export type SendEmailInput<Template extends EmailTemplate = EmailTemplate> = {
to: string
template: Template
props: EmailTemplateProps[Template]
config: EmailSendConfig
subject?: string
}
export async function sendEmail<Template extends EmailTemplate>(input: SendEmailInput<Template>) {
const to = input.to.trim()
if (!to) {
return
}
const subject = input.subject ?? emailSubjects[input.template](input.props)
const replyTo = emailReplyTo[input.template](input.props)?.trim() || undefined
const provider = getEmailProvider(input.config)
if (provider === "dev") {
console.info(`[email] dev email payload for ${to}: ${JSON.stringify({ template: input.template, subject, replyTo, props: input.props })}`)
return
}
const component = renderEmailTemplate(input.template, input.props)
const [html, text] = await Promise.all([
render(component),
render(component, { plainText: true }),
])
if (provider === "resend") {
await sendViaResend({ to, subject, replyTo, html, text, template: input.template, config: input.config })
return
}
await sendViaNodemailer({ to, subject, replyTo, html, text, template: input.template, config: input.config })
}
function getEmailProvider(config: EmailSendConfig): EmailProvider {
if (config.smtp?.host?.trim()) {
return "nodemailer"
}
if (config.resendApiKey?.trim()) {
return "resend"
}
if (config.devMode) {
return "dev"
}
return "nodemailer"
}
async function sendViaResend(input: {
to: string
subject: string
replyTo?: string
html: string
text: string
template: EmailTemplate
config: EmailSendConfig
}) {
const from = input.config.from
const apiKey = input.config.resendApiKey
if (!apiKey || !from) {
throw emailNotConfigured({
template: input.template,
recipient: input.to,
detail: "Resend transactional email requires EMAIL_FROM and RESEND_API_KEY",
})
}
try {
const resend = new Resend(apiKey)
const result = await resend.emails.send({
from,
to: input.to,
subject: input.subject,
replyTo: input.replyTo,
html: input.html,
text: input.text,
})
if (result.error) {
throw new EmailSendError({
template: input.template,
reason: "resend_rejected",
recipient: input.to,
detail: result.error.message,
})
}
} catch (error) {
if (error instanceof EmailSendError) {
throw error
}
const message = error instanceof Error ? error.message : "Unknown error"
throw new EmailSendError({ template: input.template, reason: "resend_network", recipient: input.to, detail: message })
}
}
async function sendViaNodemailer(input: {
to: string
subject: string
replyTo?: string
html: string
text: string
template: EmailTemplate
config: EmailSendConfig
}) {
const from = input.config.from
const smtp = input.config.smtp
if (!from || !smtp?.host) {
throw emailNotConfigured({
template: input.template,
recipient: input.to,
detail: "SMTP transactional email requires EMAIL_FROM and SMTP_HOST, or configure RESEND_API_KEY",
})
}
try {
const transporter = nodemailer.createTransport({
host: smtp.host,
port: smtp.port ?? 587,
secure: smtp.secure ?? false,
auth: smtp.user
? {
user: smtp.user,
pass: smtp.pass,
}
: undefined,
})
await transporter.sendMail({
from,
to: input.to,
subject: input.subject,
replyTo: input.replyTo,
html: input.html,
text: input.text,
})
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error"
throw new EmailSendError({ template: input.template, reason: "nodemailer_rejected", recipient: input.to, detail: message })
}
}
@@ -0,0 +1,33 @@
export const DEFAULT_OPENWORK_DOWNLOAD_URL = "https://openworklabs.com/download"
export type DesktopCapability = {
title: string
description: string
}
export const desktopCapabilities: DesktopCapability[] = [
{
title: "Edit spreadsheets",
description: "Create, clean, and transform CSV and Excel files.",
},
{
title: "Control your browser",
description: "Automate the built-in browser for repetitive web tasks.",
},
{
title: "Organize files",
description: "Read, write, and manage files and folders.",
},
{
title: "Automate tasks",
description: "Build reusable workflows with skills and commands.",
},
{
title: "Generate content",
description: "Draft documents, emails, and reports.",
},
{
title: "Connect to APIs",
description: "Plug into external services and tools via MCP.",
},
]
@@ -0,0 +1,106 @@
import type { CSSProperties } from "react"
import { Body, Button, Container, Head, Heading, Html, Preview, Section, Text } from "@react-email/components"
import { desktopCapabilities } from "./desktop-capabilities.js"
export type DownloadLinkEmailProps = {
organizationName?: string
downloadUrl: string
}
export function DownloadLinkEmail({ organizationName, downloadUrl }: DownloadLinkEmailProps) {
const intro = organizationName
? `Here's your link to download the OpenWork desktop app for ${organizationName}.`
: "Here's your link to download the OpenWork desktop app."
return (
<Html>
<Head />
<Preview>Your OpenWork download link</Preview>
<Body style={styles.body}>
<Container style={styles.container}>
<Text style={styles.eyebrow}>OpenWork desktop</Text>
<Heading style={styles.heading}>Download OpenWork</Heading>
<Text style={styles.text}>{intro}</Text>
<Section style={styles.capabilitiesBox}>
{desktopCapabilities.map((capability) => (
<Text key={capability.title} style={styles.capability}>
<strong>{capability.title}</strong> — {capability.description}
</Text>
))}
</Section>
<Button href={downloadUrl} style={styles.button}>Download OpenWork</Button>
<Text style={styles.footer}>Signing in inside the app syncs your team&apos;s shared skills.</Text>
</Container>
</Body>
</Html>
)
}
const styles = {
body: {
backgroundColor: "#f6f4ef",
color: "#171412",
fontFamily: "Arial, sans-serif",
margin: 0,
},
container: {
backgroundColor: "#fffdf8",
border: "1px solid #e8dfd0",
borderRadius: "20px",
margin: "40px auto",
maxWidth: "560px",
padding: "32px",
},
eyebrow: {
color: "#8a5a28",
fontSize: "13px",
fontWeight: 700,
letterSpacing: "0.08em",
margin: "0 0 12px",
textTransform: "uppercase",
},
heading: {
color: "#171412",
fontSize: "28px",
lineHeight: "34px",
margin: "0 0 16px",
},
text: {
color: "#4d4640",
fontSize: "16px",
lineHeight: "24px",
margin: "0 0 24px",
},
capabilitiesBox: {
backgroundColor: "#f8f1e6",
border: "1px solid #eadcc8",
borderRadius: "16px",
margin: "0 0 24px",
padding: "20px",
},
capability: {
color: "#4d4640",
fontSize: "14px",
lineHeight: "21px",
margin: "0 0 8px",
},
button: {
backgroundColor: "#171412",
borderRadius: "999px",
color: "#fff8eb",
display: "inline-block",
fontSize: "15px",
fontWeight: 700,
marginBottom: "24px",
padding: "13px 22px",
textDecoration: "none",
},
footer: {
color: "#756c62",
fontSize: "14px",
lineHeight: "21px",
margin: 0,
},
} satisfies Record<string, CSSProperties>
+160
View File
@@ -0,0 +1,160 @@
import { Body, Container, Head, Heading, Hr, Html, Preview, Section, Text } from "@react-email/components"
export type FeedbackEmailProps = {
name: string
email: string
message: string
mode?: "feedback" | "contact"
source: string
entrypoint: string
deployment: string
appVersion: string
openworkServerVersion: string
opencodeVersion: string
osName: string
osVersion: string
platform: string
diagnosticsSummary: string
submittedAt: string
}
export function FeedbackEmail({
name,
email,
message,
mode = "feedback",
source,
entrypoint,
deployment,
appVersion,
openworkServerVersion,
opencodeVersion,
osName,
osVersion,
platform,
diagnosticsSummary,
submittedAt,
}: FeedbackEmailProps) {
const osLabel = [osName, osVersion].filter(Boolean).join(" ")
const isContact = mode === "contact"
const label = isContact ? "contact message" : "feedback"
const metadata = [
["Source", source],
["Entrypoint", entrypoint],
["Deployment", deployment],
["App version", appVersion],
["OpenWork server", openworkServerVersion],
["OpenCode", opencodeVersion],
["OS", osLabel],
["Platform", platform],
["Submitted", submittedAt],
].filter(([, value]) => value)
return (
<Html>
<Head />
<Preview>{name} sent an OpenWork {label} from {entrypoint || source || "unknown"}</Preview>
<Body style={styles.body}>
<Container style={styles.container}>
<Text style={styles.eyebrow}>{isContact ? "OpenWork contact" : "OpenWork feedback"}</Text>
<Heading style={styles.heading}>{isContact ? "Contact message" : "Feedback"} from {name}</Heading>
<Text style={styles.contact}>{email}</Text>
<Section style={styles.messageBox}>
<Text style={styles.message}>{message}</Text>
</Section>
<Hr style={styles.hr} />
<Text style={styles.sectionTitle}>Context</Text>
{metadata.map(([label, value]) => (
<Text key={label} style={styles.metaLine}>
<strong>{label}:</strong> {value}
</Text>
))}
{diagnosticsSummary ? (
<>
<Hr style={styles.hr} />
<Text style={styles.sectionTitle}>Diagnostics summary</Text>
<Text style={styles.diagnostics}>{diagnosticsSummary}</Text>
</>
) : null}
</Container>
</Body>
</Html>
)
}
const styles = {
body: {
backgroundColor: "#f6f4ef",
color: "#171412",
fontFamily: "Arial, sans-serif",
margin: 0,
},
container: {
backgroundColor: "#fffdf8",
border: "1px solid #e8dfd0",
borderRadius: "20px",
margin: "40px auto",
maxWidth: "620px",
padding: "32px",
},
eyebrow: {
color: "#8a5a28",
fontSize: "13px",
fontWeight: 700,
letterSpacing: "0.08em",
margin: "0 0 12px",
textTransform: "uppercase" as const,
},
heading: {
color: "#171412",
fontSize: "28px",
lineHeight: "34px",
margin: "0 0 8px",
},
contact: {
color: "#756c62",
fontSize: "14px",
lineHeight: "21px",
margin: "0 0 24px",
},
messageBox: {
backgroundColor: "#171412",
borderRadius: "16px",
margin: "0 0 24px",
padding: "20px",
},
message: {
color: "#fff8eb",
fontSize: "16px",
lineHeight: "24px",
margin: 0,
whiteSpace: "pre-wrap" as const,
},
hr: {
borderColor: "#e8dfd0",
margin: "24px 0 18px",
},
sectionTitle: {
color: "#171412",
fontSize: "15px",
fontWeight: 700,
margin: "0 0 10px",
},
metaLine: {
color: "#4d4640",
fontSize: "14px",
lineHeight: "21px",
margin: "0 0 6px",
},
diagnostics: {
color: "#4d4640",
fontFamily: "Menlo, Consolas, monospace",
fontSize: "12px",
lineHeight: "18px",
margin: 0,
whiteSpace: "pre-wrap" as const,
},
}
+53
View File
@@ -0,0 +1,53 @@
import { createElement, type ReactElement } from "react"
import { DownloadLinkEmail, type DownloadLinkEmailProps } from "./download-link.js"
import { FeedbackEmail, type FeedbackEmailProps } from "./feedback.js"
import { OrganizationInviteEmail, type OrganizationInviteEmailProps } from "./organization-invite.js"
import { PasswordResetEmail, type PasswordResetEmailProps } from "./password-reset.js"
import { VerificationEmail, type VerificationEmailProps } from "./verification.js"
export type { DownloadLinkEmailProps } from "./download-link.js"
export type { FeedbackEmailProps } from "./feedback.js"
export type { OrganizationInviteEmailProps } from "./organization-invite.js"
export type { PasswordResetEmailProps } from "./password-reset.js"
export type { VerificationEmailProps } from "./verification.js"
export type EmailTemplateProps = {
verification: VerificationEmailProps
passwordReset: PasswordResetEmailProps
organizationInvite: OrganizationInviteEmailProps
downloadLink: DownloadLinkEmailProps
feedback: FeedbackEmailProps
}
export type EmailTemplate = keyof EmailTemplateProps
export const emailSubjects: { [Template in EmailTemplate]: (props: EmailTemplateProps[Template]) => string } = {
verification: ({ verificationCode }) => `Your OpenWork verification code is ${verificationCode}`,
passwordReset: () => "Reset your OpenWork password",
organizationInvite: ({ organizationName }) => `You're invited to join ${organizationName} on OpenWork`,
downloadLink: () => "Your OpenWork download link",
feedback: ({ name, source }) => `OpenWork feedback from ${name}${source ? ` (${source})` : ""}`,
}
export const emailReplyTo: { [Template in EmailTemplate]: (props: EmailTemplateProps[Template]) => string | undefined } = {
verification: () => undefined,
passwordReset: () => undefined,
organizationInvite: () => undefined,
downloadLink: () => undefined,
feedback: ({ email }) => email,
}
const emailRenderers: { [Template in EmailTemplate]: (props: EmailTemplateProps[Template]) => ReactElement } = {
verification: (props) => createElement(VerificationEmail, props),
passwordReset: (props) => createElement(PasswordResetEmail, props),
organizationInvite: (props) => createElement(OrganizationInviteEmail, props),
downloadLink: (props) => createElement(DownloadLinkEmail, props),
feedback: (props) => createElement(FeedbackEmail, props),
}
export function renderEmailTemplate<Template extends EmailTemplate>(
template: Template,
props: EmailTemplateProps[Template],
): ReactElement {
return emailRenderers[template](props)
}
@@ -0,0 +1,152 @@
import React, { type CSSProperties } from "react"
import { Body, Button, Container, Head, Heading, Hr, Html, Img, Preview, Section, Text } from "@react-email/components"
const LOGO_URL = "https://openworklabs.com/email/openwork-mark.png"
export type OrganizationInviteEmailProps = {
inviteLink: string
invitedByName: string
invitedByEmail: string
organizationName: string
role: string
}
export function OrganizationInviteEmail({
inviteLink,
invitedByName,
invitedByEmail,
organizationName,
role,
}: OrganizationInviteEmailProps) {
const inviter = invitedByEmail ? `${invitedByName} (${invitedByEmail})` : invitedByName
return (
<Html>
<Head />
<Preview>{invitedByName} invited you to join {organizationName} on OpenWork</Preview>
<Body style={styles.body}>
<Container style={styles.frame}>
<Section style={styles.brand}>
<Img src={LOGO_URL} width="31" height="24" alt="OpenWork" style={styles.brandLogo} />
<span style={styles.brandName}>OpenWork</span>
</Section>
<Section style={styles.card}>
<Text style={styles.eyebrow}>Invitation</Text>
<Heading style={styles.heading}>Join {organizationName} on OpenWork</Heading>
<Text style={styles.text}>
{inviter} invited you to join the <span style={styles.strong}>{organizationName}</span> workspace as {articleFor(role)} {role}.
</Text>
<Button href={inviteLink} style={styles.button}>Accept invite</Button>
<Hr style={styles.hr} />
<Text style={styles.fallback}>If the button doesn&apos;t work, paste this link into your browser:</Text>
<Text style={styles.link}>{inviteLink}</Text>
</Section>
<Text style={styles.footer}>
You received this email because someone invited you to an OpenWork workspace.
<br />
OpenWork · openworklabs.com
</Text>
</Container>
</Body>
</Html>
)
}
function articleFor(value: string) {
return /^[aeiou]/i.test(value.trim()) ? "an" : "a"
}
const styles = {
body: {
backgroundColor: "#F0F1F3",
color: "#1C2024",
fontFamily: "'IBM Plex Sans', -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif",
margin: 0,
},
frame: {
margin: "0 auto",
maxWidth: "560px",
padding: "56px 0",
},
brand: {
marginBottom: "20px",
paddingLeft: "4px",
},
brandLogo: {
display: "inline-block",
verticalAlign: "middle",
},
brandName: {
color: "#1C2024",
fontSize: "15px",
fontWeight: 600,
marginLeft: "8px",
verticalAlign: "middle",
},
card: {
backgroundColor: "#FFFFFF",
border: "1px solid #E1E4E8",
borderRadius: "16px",
padding: "40px",
},
eyebrow: {
color: "#60646C",
fontSize: "12px",
fontWeight: 500,
letterSpacing: "0.08em",
margin: "0 0 12px",
textTransform: "uppercase",
},
heading: {
color: "#1C2024",
fontSize: "24px",
fontWeight: 600,
letterSpacing: "-0.01em",
lineHeight: "32px",
margin: "0 0 12px",
},
text: {
color: "#60646C",
fontSize: "15px",
lineHeight: "24px",
margin: "0 0 28px",
},
strong: {
color: "#1C2024",
fontWeight: 500,
},
button: {
backgroundColor: "#1C2024",
borderRadius: "10px",
color: "#FFFFFF",
display: "inline-block",
fontSize: "15px",
fontWeight: 500,
padding: "12px 24px",
textDecoration: "none",
},
hr: {
borderColor: "#E1E4E8",
margin: "32px 0 20px",
},
fallback: {
color: "#60646C",
fontSize: "13px",
lineHeight: "20px",
margin: "0 0 4px",
},
link: {
color: "#3E63DD",
fontSize: "13px",
lineHeight: "20px",
margin: 0,
wordBreak: "break-all",
},
footer: {
color: "#8B8D98",
fontSize: "12px",
lineHeight: "18px",
margin: "24px 0 0",
paddingLeft: "4px",
},
} satisfies Record<string, CSSProperties>
@@ -0,0 +1,91 @@
import { Body, Button, Container, Head, Heading, Hr, Html, Preview, Text } from "@react-email/components"
export type PasswordResetEmailProps = {
resetLink: string
}
export function PasswordResetEmail({ resetLink }: PasswordResetEmailProps) {
return (
<Html>
<Head />
<Preview>Reset your OpenWork password</Preview>
<Body style={styles.body}>
<Container style={styles.container}>
<Text style={styles.eyebrow}>OpenWork account</Text>
<Heading style={styles.heading}>Reset your password</Heading>
<Text style={styles.text}>Use this secure link to choose a new password for your OpenWork account.</Text>
<Button href={resetLink} style={styles.button}>Reset password</Button>
<Text style={styles.footer}>This link expires in 1 hour. If you did not request a password reset, you can ignore this email.</Text>
<Hr style={styles.hr} />
<Text style={styles.footer}>If the button does not work, paste this link into your browser:</Text>
<Text style={styles.link}>{resetLink}</Text>
</Container>
</Body>
</Html>
)
}
const styles = {
body: {
backgroundColor: "#f6f4ef",
color: "#171412",
fontFamily: "Arial, sans-serif",
margin: 0,
},
container: {
backgroundColor: "#fffdf8",
border: "1px solid #e8dfd0",
borderRadius: "20px",
margin: "40px auto",
maxWidth: "560px",
padding: "32px",
},
eyebrow: {
color: "#8a5a28",
fontSize: "13px",
fontWeight: 700,
letterSpacing: "0.08em",
margin: "0 0 12px",
textTransform: "uppercase" as const,
},
heading: {
color: "#171412",
fontSize: "28px",
lineHeight: "34px",
margin: "0 0 16px",
},
text: {
color: "#4d4640",
fontSize: "16px",
lineHeight: "24px",
margin: "0 0 24px",
},
button: {
backgroundColor: "#171412",
borderRadius: "999px",
color: "#fff8eb",
display: "inline-block",
fontSize: "15px",
fontWeight: 700,
marginBottom: "24px",
padding: "13px 22px",
textDecoration: "none",
},
hr: {
borderColor: "#e8dfd0",
margin: "28px 0 18px",
},
footer: {
color: "#756c62",
fontSize: "14px",
lineHeight: "21px",
margin: "0 0 8px",
},
link: {
color: "#5b3a18",
fontSize: "13px",
lineHeight: "19px",
margin: 0,
wordBreak: "break-all" as const,
},
}
@@ -0,0 +1,83 @@
import { Body, Container, Head, Heading, Html, Preview, Section, Text } from "@react-email/components"
export type VerificationEmailProps = {
verificationCode: string
}
export function VerificationEmail({ verificationCode }: VerificationEmailProps) {
return (
<Html>
<Head />
<Preview>Your OpenWork verification code is {verificationCode}</Preview>
<Body style={styles.body}>
<Container style={styles.container}>
<Text style={styles.eyebrow}>OpenWork</Text>
<Heading style={styles.heading}>Verify your email</Heading>
<Text style={styles.text}>Enter this code to finish signing in to OpenWork.</Text>
<Section style={styles.codeBox}>
<Text style={styles.code}>{verificationCode}</Text>
</Section>
<Text style={styles.footer}>This code expires in 10 minutes. If you did not request it, you can ignore this email.</Text>
</Container>
</Body>
</Html>
)
}
const styles = {
body: {
backgroundColor: "#f6f4ef",
color: "#171412",
fontFamily: "Arial, sans-serif",
margin: 0,
},
container: {
backgroundColor: "#fffdf8",
border: "1px solid #e8dfd0",
borderRadius: "20px",
margin: "40px auto",
maxWidth: "520px",
padding: "32px",
},
eyebrow: {
color: "#8a5a28",
fontSize: "13px",
fontWeight: 700,
letterSpacing: "0.08em",
margin: "0 0 12px",
textTransform: "uppercase" as const,
},
heading: {
color: "#171412",
fontSize: "28px",
lineHeight: "34px",
margin: "0 0 16px",
},
text: {
color: "#4d4640",
fontSize: "16px",
lineHeight: "24px",
margin: "0 0 24px",
},
codeBox: {
backgroundColor: "#171412",
borderRadius: "16px",
margin: "0 0 24px",
padding: "22px",
textAlign: "center" as const,
},
code: {
color: "#fff8eb",
fontSize: "34px",
fontWeight: 700,
letterSpacing: "0.18em",
lineHeight: "40px",
margin: 0,
},
footer: {
color: "#756c62",
fontSize: "14px",
lineHeight: "21px",
margin: 0,
},
}
+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"esModuleInterop": true,
"jsx": "react-jsx",
"skipLibCheck": true,
"declaration": true,
"outDir": "dist"
},
"include": ["src", "emails"]
}
+12
View File
@@ -0,0 +1,12 @@
import { defineConfig } from "tsup"
export default defineConfig({
clean: true,
dts: true,
entry: {
index: "src/index.ts",
"templates/index": "src/templates/index.ts",
},
format: ["esm"],
target: "es2022",
})