feat(mail): templates HTML React Email + brand "Rubis sur l'ongle"
Templates HTML stylés DA Rubis pour les 2 emails sortants — fini le
plain text moche.
apps/api/app/mails/
├── _brand.ts : tokens couleur + spacing partagés
├── _layout.tsx : squelette commun (header rubis-deep + footer)
├── checkin_email.tsx : email envoyé À L'USER avec 2 boutons CTA
│ Oui (rubis primary) / Non (outlined)
└── relance_email.tsx : email envoyé AU CLIENT, body texte du plan
+ card récap (numéro, montant, échéance,
badge retard rubis-deep)
Stack :
- @react-email/components + @react-email/render
- Tous les styles inline (compatible Gmail / Outlook / Apple Mail)
- HTML + plain text en fallback (anti-spam, accessibility)
mail_dispatcher.ts :
- sendRelanceEmail : .html(rendered) + .text(body)
- sendCheckinEmail : .html(rendered) + .text(body)
- daysLate calculé via clock.now (démo-aware)
send_test_email :
- Nouveau flag --template=checkin (default) | relance | plain pour
tester chaque rendu via Mailpit sans créer de vraie facture.
Brand & landing :
- "Rubis Sur l'Ongle" → "Rubis sur l'ongle" partout (config, mail,
PDF, Stripe appInfo)
- Nouvelle env var LANDING_URL (default https://rubis.arthurbarre.fr)
- Footer email rend "Rubis sur l'ongle" comme <a> rubis cliquable
vers la landing — l'user qui reçoit le mail connaît la marque
derrière l'envoi
- .env.example mis à jour avec LANDING_URL pour les autres devs
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
87c6f49692
commit
ff8fe64be2
@ -52,7 +52,7 @@ S3_FORCE_PATH_STYLE=true
|
||||
# Mail (Resend par défaut, Mailpit en fallback dev via MAIL_DRIVER=smtp)
|
||||
#--------------------------------------------------------------------
|
||||
MAIL_FROM_ADDRESS=rubis@arthurbarre.fr
|
||||
MAIL_FROM_NAME=Rubis Sur l'Ongle
|
||||
MAIL_FROM_NAME=Rubis sur l'ongle
|
||||
MAIL_DRIVER=resend
|
||||
RESEND_API_KEY=
|
||||
# Fallback Mailpit (si MAIL_DRIVER=smtp)
|
||||
@ -70,6 +70,12 @@ MISTRAL_API_KEY=
|
||||
#--------------------------------------------------------------------
|
||||
WEB_URL=http://localhost:5173
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
# Landing publique — lien dans le footer des emails ("Rubis sur l'ongle"
|
||||
# pointe vers ce domaine).
|
||||
#--------------------------------------------------------------------
|
||||
LANDING_URL=https://rubis.arthurbarre.fr
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
# Auth (refresh tokens)
|
||||
#--------------------------------------------------------------------
|
||||
|
||||
40
apps/api/app/mails/_brand.ts
Normal file
40
apps/api/app/mails/_brand.ts
Normal file
@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Tokens de marque partagés par tous les templates email.
|
||||
*
|
||||
* On reste sur des hex en dur (pas de CSS vars) parce que les clients mail
|
||||
* (Gmail, Outlook) ne supportent pas les `--var` dans les `style="..."`.
|
||||
* Tous les styles sont inline pour la même raison.
|
||||
*/
|
||||
|
||||
export const BRAND = {
|
||||
// Palette Rubis (cf. CLAUDE.md → marque)
|
||||
rubis: '#9F1239',
|
||||
rubisDeep: '#771328',
|
||||
rubisLight: '#C9415C',
|
||||
rubisGlow: '#FBE4EA',
|
||||
cream: '#FAF7F2',
|
||||
cream2: '#F5EFE7',
|
||||
ink: '#1A1410',
|
||||
ink2: '#4F4640',
|
||||
ink3: '#8A7F76',
|
||||
line: '#E8E0D6',
|
||||
white: '#FFFFFF',
|
||||
|
||||
// Typo — on s'appuie sur les fallbacks system-ui pour la portabilité mail.
|
||||
fontBody:
|
||||
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Helvetica, Arial, sans-serif",
|
||||
|
||||
// Radius cohérents avec l'app
|
||||
radiusButton: '6px',
|
||||
radiusCard: '14px',
|
||||
} as const
|
||||
|
||||
/** Wrappers d'unités inline-friendly pour pas faire de gros calculs côté <style>. */
|
||||
export const sp = {
|
||||
xs: '4px',
|
||||
sm: '8px',
|
||||
md: '12px',
|
||||
lg: '16px',
|
||||
xl: '24px',
|
||||
xxl: '32px',
|
||||
} as const
|
||||
167
apps/api/app/mails/_layout.tsx
Normal file
167
apps/api/app/mails/_layout.tsx
Normal file
@ -0,0 +1,167 @@
|
||||
/**
|
||||
* Squelette commun aux 2 templates Rubis : header rubis-deep avec brand
|
||||
* + container cream + footer "Émis via Rubis sur l'ongle" (cliquable
|
||||
* vers la landing publique).
|
||||
*
|
||||
* Approche : on stylé en inline via les `style` props que React Email
|
||||
* convertit en HTML inline (compatible tous mail clients y compris
|
||||
* Outlook). Pas de classes Tailwind ici — risquées en mail.
|
||||
*/
|
||||
|
||||
import * as React from 'react'
|
||||
import {
|
||||
Html,
|
||||
Head,
|
||||
Preview,
|
||||
Body,
|
||||
Container,
|
||||
Section,
|
||||
Row,
|
||||
Column,
|
||||
Text,
|
||||
Link,
|
||||
} from '@react-email/components'
|
||||
|
||||
import { BRAND, sp } from './_brand.js'
|
||||
|
||||
type LayoutProps = {
|
||||
/** Aperçu dans la liste mail (Gmail preview text). */
|
||||
preview: string
|
||||
/** Nom commercial affiché dans le header (vendeur ou "Rubis"). */
|
||||
brandName: string
|
||||
/** Sous-titre header (ex: numéro de facture, date). Optionnel. */
|
||||
brandSubtitle?: string | null
|
||||
/** URL de la landing publique — lien dans le footer ("Rubis sur l'ongle"). */
|
||||
landingUrl?: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function EmailLayout({
|
||||
preview,
|
||||
brandName,
|
||||
brandSubtitle,
|
||||
landingUrl,
|
||||
children,
|
||||
}: LayoutProps) {
|
||||
return (
|
||||
<Html lang="fr">
|
||||
<Head />
|
||||
<Preview>{preview}</Preview>
|
||||
<Body style={bodyStyle}>
|
||||
<Container style={containerStyle}>
|
||||
{/* Bandeau rubis-deep avec gem ◆ + brand */}
|
||||
<Section style={headerStyle}>
|
||||
<Row>
|
||||
<Column style={{ verticalAlign: 'middle' }}>
|
||||
<Text style={headerBrandStyle}>
|
||||
<span style={gemStyle}>◆</span>
|
||||
{brandName}
|
||||
</Text>
|
||||
{brandSubtitle ? (
|
||||
<Text style={headerSubtitleStyle}>{brandSubtitle}</Text>
|
||||
) : null}
|
||||
</Column>
|
||||
</Row>
|
||||
</Section>
|
||||
|
||||
{/* Corps de l'email */}
|
||||
<Section style={contentStyle}>{children}</Section>
|
||||
|
||||
{/* Footer Rubis — "Rubis sur l'ongle" cliquable vers la landing */}
|
||||
<Section style={footerStyle}>
|
||||
<Text style={footerTextStyle}>
|
||||
Émis via{' '}
|
||||
{landingUrl ? (
|
||||
<Link href={landingUrl} style={footerLinkStyle}>
|
||||
Rubis sur l'ongle
|
||||
</Link>
|
||||
) : (
|
||||
<strong style={{ color: BRAND.ink2 }}>Rubis sur l'ongle</strong>
|
||||
)}
|
||||
{' — '}
|
||||
<span style={{ color: BRAND.ink3 }}>
|
||||
vos factures relancées toutes seules pendant que vous travaillez.
|
||||
</span>
|
||||
</Text>
|
||||
</Section>
|
||||
</Container>
|
||||
</Body>
|
||||
</Html>
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Styles inline
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const bodyStyle: React.CSSProperties = {
|
||||
backgroundColor: BRAND.cream,
|
||||
fontFamily: BRAND.fontBody,
|
||||
margin: 0,
|
||||
padding: `${sp.xl} 0`,
|
||||
color: BRAND.ink,
|
||||
}
|
||||
|
||||
const containerStyle: React.CSSProperties = {
|
||||
backgroundColor: BRAND.white,
|
||||
borderRadius: BRAND.radiusCard,
|
||||
border: `1px solid ${BRAND.line}`,
|
||||
margin: '0 auto',
|
||||
maxWidth: '560px',
|
||||
overflow: 'hidden',
|
||||
}
|
||||
|
||||
const headerStyle: React.CSSProperties = {
|
||||
backgroundColor: BRAND.rubisDeep,
|
||||
padding: `${sp.xl} ${sp.xl}`,
|
||||
}
|
||||
|
||||
const headerBrandStyle: React.CSSProperties = {
|
||||
color: BRAND.white,
|
||||
fontSize: '20px',
|
||||
fontWeight: 800,
|
||||
letterSpacing: '-0.01em',
|
||||
margin: 0,
|
||||
lineHeight: '1.1',
|
||||
}
|
||||
|
||||
const gemStyle: React.CSSProperties = {
|
||||
display: 'inline-block',
|
||||
marginRight: sp.sm,
|
||||
color: BRAND.rubisGlow,
|
||||
fontSize: '18px',
|
||||
verticalAlign: '-1px',
|
||||
}
|
||||
|
||||
const headerSubtitleStyle: React.CSSProperties = {
|
||||
color: BRAND.cream2,
|
||||
fontSize: '12px',
|
||||
margin: `${sp.xs} 0 0 0`,
|
||||
letterSpacing: '0.04em',
|
||||
textTransform: 'uppercase',
|
||||
fontWeight: 600,
|
||||
}
|
||||
|
||||
const contentStyle: React.CSSProperties = {
|
||||
padding: `${sp.xl} ${sp.xl}`,
|
||||
}
|
||||
|
||||
const footerStyle: React.CSSProperties = {
|
||||
borderTop: `1px solid ${BRAND.line}`,
|
||||
backgroundColor: BRAND.cream2,
|
||||
padding: `${sp.lg} ${sp.xl}`,
|
||||
}
|
||||
|
||||
const footerTextStyle: React.CSSProperties = {
|
||||
color: BRAND.ink3,
|
||||
fontSize: '11px',
|
||||
lineHeight: '1.5',
|
||||
margin: 0,
|
||||
textAlign: 'center',
|
||||
}
|
||||
|
||||
const footerLinkStyle: React.CSSProperties = {
|
||||
color: BRAND.rubis,
|
||||
fontWeight: 600,
|
||||
textDecoration: 'none',
|
||||
}
|
||||
213
apps/api/app/mails/checkin_email.tsx
Normal file
213
apps/api/app/mails/checkin_email.tsx
Normal file
@ -0,0 +1,213 @@
|
||||
/**
|
||||
* Template email check-in — envoyé à L'UTILISATEUR (le patron de TPE)
|
||||
* pour lui demander si une facture donnée a été payée AVANT que la 1re
|
||||
* relance ne parte chez son client.
|
||||
*
|
||||
* Structure :
|
||||
* - Header rubis-deep "Rubis · Confirmation requise"
|
||||
* - Eyebrow + question Avez-vous été payé ?
|
||||
* - Card facture (numéro + montant + client + échéance)
|
||||
* - 2 gros boutons CTA :
|
||||
* • "✓ Oui — la facture est payée" → /paid (status=paid + cancel relances)
|
||||
* • "→ Non — toujours impayée, lance les relances" → /pending
|
||||
* - Mention TTL 24h
|
||||
*/
|
||||
|
||||
import * as React from 'react'
|
||||
import { Section, Text, Button, Heading, Hr } from '@react-email/components'
|
||||
|
||||
import { BRAND, sp } from './_brand.js'
|
||||
import { EmailLayout } from './_layout.js'
|
||||
|
||||
export type CheckinEmailProps = {
|
||||
invoice: {
|
||||
numero: string
|
||||
amountFormatted: string
|
||||
dueDateFormatted: string
|
||||
}
|
||||
client: { name: string }
|
||||
user: { fullName: string | null }
|
||||
paidUrl: string
|
||||
pendingUrl: string
|
||||
/** URL landing publique (footer cliquable "Rubis sur l'ongle"). */
|
||||
landingUrl?: string
|
||||
}
|
||||
|
||||
export function CheckinEmail({
|
||||
invoice,
|
||||
client,
|
||||
user,
|
||||
paidUrl,
|
||||
pendingUrl,
|
||||
landingUrl,
|
||||
}: CheckinEmailProps) {
|
||||
const greeting = user.fullName ? `Bonjour ${user.fullName.split(' ')[0]},` : 'Bonjour,'
|
||||
|
||||
return (
|
||||
<EmailLayout
|
||||
preview={`${invoice.numero} (${invoice.amountFormatted}) — payée par ${client.name} ?`}
|
||||
brandName="Rubis"
|
||||
brandSubtitle="Confirmation requise"
|
||||
landingUrl={landingUrl}
|
||||
>
|
||||
<Text style={greetingStyle}>{greeting}</Text>
|
||||
|
||||
<Heading as="h1" style={titleStyle}>
|
||||
Avez-vous été <em style={emStyle}>payé</em> sur cette facture ?
|
||||
</Heading>
|
||||
|
||||
<Text style={leadStyle}>
|
||||
Aucune relance ne part sans votre validation. Si la facture est déjà
|
||||
réglée, on évite l'email inutile et on encaisse +1 rubis.
|
||||
</Text>
|
||||
|
||||
{/* Card facture */}
|
||||
<Section style={invoiceCardStyle}>
|
||||
<Text style={invoiceNumeroStyle}>{invoice.numero}</Text>
|
||||
<Text style={invoiceClientStyle}>{client.name}</Text>
|
||||
<Text style={invoiceAmountStyle}>{invoice.amountFormatted}</Text>
|
||||
<Text style={invoiceDueStyle}>
|
||||
Échéance le <strong>{invoice.dueDateFormatted}</strong>
|
||||
</Text>
|
||||
</Section>
|
||||
|
||||
{/* CTA boutons */}
|
||||
<Section style={ctaSectionStyle}>
|
||||
<Button href={paidUrl} style={primaryButtonStyle}>
|
||||
✓ Oui, la facture est payée
|
||||
</Button>
|
||||
</Section>
|
||||
<Section style={{ marginTop: sp.md }}>
|
||||
<Button href={pendingUrl} style={secondaryButtonStyle}>
|
||||
Non, toujours en attente — lance les relances
|
||||
</Button>
|
||||
</Section>
|
||||
|
||||
<Hr style={hrStyle} />
|
||||
|
||||
<Text style={footnoteStyle}>
|
||||
Ces liens expirent dans 24h. Vous pouvez aussi répondre directement
|
||||
depuis l'app sur la fiche facture.
|
||||
</Text>
|
||||
</EmailLayout>
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Styles inline
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const greetingStyle: React.CSSProperties = {
|
||||
fontSize: '14px',
|
||||
color: BRAND.ink2,
|
||||
margin: `0 0 ${sp.md} 0`,
|
||||
}
|
||||
|
||||
const titleStyle: React.CSSProperties = {
|
||||
color: BRAND.ink,
|
||||
fontSize: '24px',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '-0.018em',
|
||||
lineHeight: '1.2',
|
||||
margin: `0 0 ${sp.md} 0`,
|
||||
}
|
||||
|
||||
const emStyle: React.CSSProperties = {
|
||||
color: BRAND.rubis,
|
||||
fontStyle: 'normal',
|
||||
}
|
||||
|
||||
const leadStyle: React.CSSProperties = {
|
||||
color: BRAND.ink2,
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.6',
|
||||
margin: `0 0 ${sp.xl} 0`,
|
||||
}
|
||||
|
||||
const invoiceCardStyle: React.CSSProperties = {
|
||||
backgroundColor: BRAND.cream,
|
||||
border: `1px solid ${BRAND.line}`,
|
||||
borderRadius: BRAND.radiusCard,
|
||||
padding: `${sp.lg} ${sp.xl}`,
|
||||
margin: `0 0 ${sp.xl} 0`,
|
||||
}
|
||||
|
||||
const invoiceNumeroStyle: React.CSSProperties = {
|
||||
fontSize: '13px',
|
||||
fontWeight: 600,
|
||||
color: BRAND.ink2,
|
||||
letterSpacing: '0.02em',
|
||||
margin: 0,
|
||||
}
|
||||
|
||||
const invoiceClientStyle: React.CSSProperties = {
|
||||
fontSize: '13px',
|
||||
color: BRAND.ink3,
|
||||
margin: `${sp.xs} 0 ${sp.md} 0`,
|
||||
}
|
||||
|
||||
const invoiceAmountStyle: React.CSSProperties = {
|
||||
fontSize: '28px',
|
||||
fontWeight: 800,
|
||||
letterSpacing: '-0.02em',
|
||||
color: BRAND.ink,
|
||||
margin: 0,
|
||||
lineHeight: '1',
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
}
|
||||
|
||||
const invoiceDueStyle: React.CSSProperties = {
|
||||
fontSize: '12px',
|
||||
color: BRAND.ink3,
|
||||
margin: `${sp.sm} 0 0 0`,
|
||||
}
|
||||
|
||||
const ctaSectionStyle: React.CSSProperties = {
|
||||
marginTop: 0,
|
||||
}
|
||||
|
||||
const primaryButtonStyle: React.CSSProperties = {
|
||||
backgroundColor: BRAND.rubis,
|
||||
color: BRAND.white,
|
||||
borderRadius: BRAND.radiusButton,
|
||||
display: 'block',
|
||||
textAlign: 'center',
|
||||
textDecoration: 'none',
|
||||
padding: `${sp.md} ${sp.xl}`,
|
||||
fontSize: '15px',
|
||||
fontWeight: 600,
|
||||
width: '100%',
|
||||
boxSizing: 'border-box',
|
||||
// Shadow rubis-teintée pour cohérence avec les boutons SPA
|
||||
boxShadow: '0 4px 12px rgba(159, 18, 57, 0.25)',
|
||||
}
|
||||
|
||||
const secondaryButtonStyle: React.CSSProperties = {
|
||||
backgroundColor: BRAND.white,
|
||||
color: BRAND.ink,
|
||||
border: `1px solid ${BRAND.ink}`,
|
||||
borderRadius: BRAND.radiusButton,
|
||||
display: 'block',
|
||||
textAlign: 'center',
|
||||
textDecoration: 'none',
|
||||
padding: `${sp.md} ${sp.xl}`,
|
||||
fontSize: '15px',
|
||||
fontWeight: 600,
|
||||
width: '100%',
|
||||
boxSizing: 'border-box',
|
||||
}
|
||||
|
||||
const hrStyle: React.CSSProperties = {
|
||||
borderColor: BRAND.line,
|
||||
borderStyle: 'solid',
|
||||
borderWidth: '0 0 1px 0',
|
||||
margin: `${sp.xl} 0 ${sp.lg} 0`,
|
||||
}
|
||||
|
||||
const footnoteStyle: React.CSSProperties = {
|
||||
fontSize: '11.5px',
|
||||
color: BRAND.ink3,
|
||||
lineHeight: '1.5',
|
||||
margin: 0,
|
||||
fontStyle: 'italic',
|
||||
}
|
||||
135
apps/api/app/mails/relance_email.tsx
Normal file
135
apps/api/app/mails/relance_email.tsx
Normal file
@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Template email de relance — envoyé AU CLIENT FINAL pour réclamer le
|
||||
* paiement d'une facture impayée. Le subject + le body sont définis par
|
||||
* le user dans son plan de relance (avec variables {{numero}} etc.) ; on
|
||||
* injecte ce body brut dans une mise en page Rubis :
|
||||
*
|
||||
* - Header rubis-deep avec le NOM DE L'ORG (ce que connaît le client)
|
||||
* et le numéro de facture en sous-titre
|
||||
* - Body rendu (le texte que le user a rédigé, déjà interpolé)
|
||||
* - Card "récap facture" en pied de body : numéro, montant, échéance
|
||||
*
|
||||
* Pas de boutons CTA dans la relance — le client est censé payer hors
|
||||
* mail (virement, espèces, ...). Le mail rappelle juste le contexte.
|
||||
*/
|
||||
|
||||
import * as React from 'react'
|
||||
import { Section, Text } from '@react-email/components'
|
||||
|
||||
import { BRAND, sp } from './_brand.js'
|
||||
import { EmailLayout } from './_layout.js'
|
||||
|
||||
export type RelanceEmailProps = {
|
||||
/** Nom commercial visible côté client (l'org du user). */
|
||||
brandName: string
|
||||
invoice: {
|
||||
numero: string
|
||||
amountFormatted: string
|
||||
dueDateFormatted: string
|
||||
daysLate: number
|
||||
}
|
||||
/** Texte de la relance (déjà interpolé) que le user a posé dans son plan. */
|
||||
bodyText: string
|
||||
/** URL landing publique (footer cliquable "Rubis sur l'ongle"). */
|
||||
landingUrl?: string
|
||||
}
|
||||
|
||||
export function RelanceEmail({
|
||||
brandName,
|
||||
invoice,
|
||||
bodyText,
|
||||
landingUrl,
|
||||
}: RelanceEmailProps) {
|
||||
const isLate = invoice.daysLate > 0
|
||||
return (
|
||||
<EmailLayout
|
||||
preview={`${invoice.numero} (${invoice.amountFormatted}) — ${
|
||||
isLate ? `${invoice.daysLate}j de retard` : 'à régler'
|
||||
}`}
|
||||
brandName={brandName}
|
||||
brandSubtitle={`Facture ${invoice.numero}`}
|
||||
landingUrl={landingUrl}
|
||||
>
|
||||
{/* Body texte en pre-line pour conserver les sauts de ligne tels que
|
||||
le user les a écrits. Le client lit le mot du patron, pas un
|
||||
template impersonnel. */}
|
||||
<Text style={bodyTextStyle}>{bodyText}</Text>
|
||||
|
||||
{/* Card récap en pied : tableau visuel court qui rappelle les chiffres. */}
|
||||
<Section style={summaryCardStyle}>
|
||||
<Text style={summaryRowStyle}>
|
||||
<span style={summaryLabelStyle}>Facture</span>
|
||||
<span style={summaryValueStyle}>{invoice.numero}</span>
|
||||
</Text>
|
||||
<Text style={summaryRowStyle}>
|
||||
<span style={summaryLabelStyle}>Montant TTC</span>
|
||||
<span
|
||||
style={{
|
||||
...summaryValueStyle,
|
||||
fontSize: '18px',
|
||||
fontWeight: 800,
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
}}
|
||||
>
|
||||
{invoice.amountFormatted}
|
||||
</span>
|
||||
</Text>
|
||||
<Text style={summaryRowStyle}>
|
||||
<span style={summaryLabelStyle}>Échéance</span>
|
||||
<span
|
||||
style={{
|
||||
...summaryValueStyle,
|
||||
color: isLate ? BRAND.rubisDeep : BRAND.ink,
|
||||
}}
|
||||
>
|
||||
{invoice.dueDateFormatted}
|
||||
{isLate ? (
|
||||
<span style={{ color: BRAND.rubisDeep, fontSize: '12px', marginLeft: sp.sm }}>
|
||||
({invoice.daysLate}j de retard)
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
</Text>
|
||||
</Section>
|
||||
</EmailLayout>
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Styles inline
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const bodyTextStyle: React.CSSProperties = {
|
||||
color: BRAND.ink,
|
||||
fontSize: '15px',
|
||||
lineHeight: '1.6',
|
||||
margin: `0 0 ${sp.xl} 0`,
|
||||
whiteSpace: 'pre-line', // préserve les \n du body sans nécessiter <br/>
|
||||
}
|
||||
|
||||
const summaryCardStyle: React.CSSProperties = {
|
||||
backgroundColor: BRAND.cream,
|
||||
border: `1px solid ${BRAND.line}`,
|
||||
borderRadius: BRAND.radiusCard,
|
||||
padding: `${sp.md} ${sp.lg}`,
|
||||
margin: `${sp.lg} 0 0 0`,
|
||||
}
|
||||
|
||||
const summaryRowStyle: React.CSSProperties = {
|
||||
display: 'block',
|
||||
margin: `${sp.sm} 0`,
|
||||
fontSize: '13px',
|
||||
lineHeight: '1.4',
|
||||
}
|
||||
|
||||
const summaryLabelStyle: React.CSSProperties = {
|
||||
display: 'inline-block',
|
||||
width: '110px',
|
||||
color: BRAND.ink3,
|
||||
fontWeight: 500,
|
||||
}
|
||||
|
||||
const summaryValueStyle: React.CSSProperties = {
|
||||
color: BRAND.ink,
|
||||
fontWeight: 600,
|
||||
}
|
||||
@ -2,6 +2,7 @@ import mail from '@adonisjs/mail/services/main'
|
||||
import logger from '@adonisjs/core/services/logger'
|
||||
import env from '#start/env'
|
||||
import { DateTime } from 'luxon'
|
||||
import { render } from '@react-email/components'
|
||||
import { renderTemplate, formatAmountFr, formatDateFr } from '#services/template'
|
||||
import * as clock from '#services/clock'
|
||||
import { captureEmailIfDemo } from '#services/demo/capture'
|
||||
@ -11,6 +12,9 @@ import type PlanStep from '#models/plan_step'
|
||||
import type User from '#models/user'
|
||||
import type Organization from '#models/organization'
|
||||
|
||||
import { CheckinEmail } from '#mails/checkin_email'
|
||||
import { RelanceEmail } from '#mails/relance_email'
|
||||
|
||||
type RelancePayload = {
|
||||
invoice: Invoice
|
||||
client: Client
|
||||
@ -99,13 +103,36 @@ export async function sendRelanceEmail({
|
||||
const fromAddress = env.get('MAIL_FROM_ADDRESS', 'relances@rubis-sur-l-ongle.fr')
|
||||
// Le client final connaît l'org (ex: "Arthur Barré"), pas Rubis. On utilise
|
||||
// le nom de l'org comme display name visible côté client. Fallback :
|
||||
// user.fullName, puis MAIL_FROM_NAME (= "Rubis Sur l'Ongle") en dernier
|
||||
// user.fullName, puis MAIL_FROM_NAME (= "Rubis sur l'ongle") en dernier
|
||||
// recours si l'org n'a pas de nom posé.
|
||||
// L'adresse technique reste sur notre domaine vérifié (SPF/DKIM Resend).
|
||||
const fromName =
|
||||
organization?.name?.trim() ||
|
||||
user?.fullName?.trim() ||
|
||||
env.get('MAIL_FROM_NAME', "Rubis Sur l'Ongle")
|
||||
env.get('MAIL_FROM_NAME', "Rubis sur l'ongle")
|
||||
|
||||
// Calcule daysLate pour le récap visuel dans le HTML.
|
||||
const nowOrg = await clock.now(invoice.organizationId)
|
||||
const daysLate = Math.floor(
|
||||
nowOrg.startOf('day').diff(invoice.dueDate.startOf('day'), 'days').days
|
||||
)
|
||||
|
||||
const landingUrl = env.get('LANDING_URL', 'https://rubis.arthurbarre.fr')
|
||||
|
||||
// Rendu HTML via React Email — DA Rubis (header rubis-deep + card cream).
|
||||
const htmlBody = await render(
|
||||
RelanceEmail({
|
||||
brandName: fromName,
|
||||
invoice: {
|
||||
numero: invoice.numero,
|
||||
amountFormatted: formatAmountFr(invoice.amountTtcCents),
|
||||
dueDateFormatted: formatDateFr(invoice.dueDate.toJSDate()),
|
||||
daysLate,
|
||||
},
|
||||
bodyText: body,
|
||||
landingUrl,
|
||||
})
|
||||
)
|
||||
|
||||
// FORK DÉMO — unique point où l'app dévie de la prod. Si l'org est
|
||||
// en mode démo, on capture l'email dans demo_captured_emails au lieu
|
||||
@ -147,8 +174,10 @@ export async function sendRelanceEmail({
|
||||
m.from(fromAddress, fromName)
|
||||
.to(client.email, client.name)
|
||||
.subject(subject)
|
||||
// Texte brut pour V1 — on ajoutera un template HTML quand on aura
|
||||
// décidé d'un look graphique pour les relances.
|
||||
// HTML rendu depuis le composant React Email (DA Rubis).
|
||||
.html(htmlBody)
|
||||
// Plain text fallback : améliore la délivrabilité (anti-spam) et
|
||||
// sert pour les clients qui désactivent le HTML.
|
||||
.text(body)
|
||||
// Reply-To pointe sur l'utilisateur Rubis : si le client final répond
|
||||
// à la relance, sa réponse arrive chez le patron de la TPE, pas dans
|
||||
@ -213,7 +242,27 @@ Merci,
|
||||
L'équipe Rubis`
|
||||
|
||||
const fromAddress = env.get('MAIL_FROM_ADDRESS', 'relances@rubis-sur-l-ongle.fr')
|
||||
const fromName = env.get('MAIL_FROM_NAME', "Rubis Sur l'Ongle")
|
||||
// Le check-in vient FROM Rubis (notification interne à l'user, pas au
|
||||
// client final). On garde donc le brand "Rubis sur l'ongle" comme display,
|
||||
// PAS le nom de l'org.
|
||||
const fromName = env.get('MAIL_FROM_NAME', "Rubis sur l'ongle")
|
||||
|
||||
// Rendu HTML — DA Rubis avec 2 boutons CTA Oui/Non.
|
||||
const landingUrl = env.get('LANDING_URL', 'https://rubis.arthurbarre.fr')
|
||||
const htmlBody = await render(
|
||||
CheckinEmail({
|
||||
invoice: {
|
||||
numero: invoice.numero,
|
||||
amountFormatted: formatAmountFr(invoice.amountTtcCents),
|
||||
dueDateFormatted: formatDateFr(invoice.dueDate.toJSDate()),
|
||||
},
|
||||
client: { name: client.name },
|
||||
user: { fullName: user.fullName ?? null },
|
||||
paidUrl,
|
||||
pendingUrl,
|
||||
landingUrl,
|
||||
})
|
||||
)
|
||||
|
||||
// FORK DÉMO — capture si demoMode (cf. sendRelanceEmail).
|
||||
const captured = await captureEmailIfDemo({
|
||||
@ -233,6 +282,8 @@ L'équipe Rubis`
|
||||
m.from(fromAddress, fromName)
|
||||
.to(user.email, user.fullName ?? user.email)
|
||||
.subject(subject)
|
||||
// HTML rendu via React Email (DA Rubis), texte brut en fallback.
|
||||
.html(htmlBody)
|
||||
.text(body)
|
||||
})
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@ export function getStripe(): Stripe {
|
||||
apiVersion: '2026-04-22.dahlia',
|
||||
typescript: true,
|
||||
appInfo: {
|
||||
name: 'Rubis Sur l\'Ongle',
|
||||
name: 'Rubis sur l\'ongle',
|
||||
version: '1.0.0',
|
||||
},
|
||||
})
|
||||
|
||||
@ -1,19 +1,31 @@
|
||||
import { BaseCommand, args, flags } from '@adonisjs/core/ace'
|
||||
import type { CommandOptions } from '@adonisjs/core/types/ace'
|
||||
import mail from '@adonisjs/mail/services/main'
|
||||
import { render } from '@react-email/components'
|
||||
import { DateTime } from 'luxon'
|
||||
import env from '#start/env'
|
||||
|
||||
import { CheckinEmail } from '#mails/checkin_email'
|
||||
import { RelanceEmail } from '#mails/relance_email'
|
||||
|
||||
/**
|
||||
* Envoie un email de test via le mailer courant (typiquement Resend)
|
||||
* pour valider la conf SPF/DKIM/clé API sans passer par toute la chaîne
|
||||
* facture → job BullMQ.
|
||||
* pour valider la conf SPF/DKIM/clé API ET le rendu visuel des templates
|
||||
* HTML React Email — sans passer par toute la chaîne facture → BullMQ.
|
||||
*
|
||||
* node ace send:test-email arthur@example.com
|
||||
* Usage :
|
||||
* node ace send:test-email arthur@example.com # checkin
|
||||
* node ace send:test-email arthur@example.com --template=checkin
|
||||
* node ace send:test-email arthur@example.com --template=relance
|
||||
* node ace send:test-email arthur@example.com --template=plain # ancien
|
||||
* node ace send:test-email arthur@example.com --reply-to=patron@tpe.fr
|
||||
*
|
||||
* Default `checkin` car c'est le template avec les boutons CTA — le plus
|
||||
* intéressant à valider visuellement au premier coup d'œil.
|
||||
*/
|
||||
export default class SendTestEmail extends BaseCommand {
|
||||
static commandName = 'send:test-email'
|
||||
static description = 'Envoie un email de test via le mailer configuré (Resend en prod)'
|
||||
static description = 'Envoie un email de test (HTML React Email) pour valider rendu + driver'
|
||||
|
||||
static options: CommandOptions = {
|
||||
startApp: true,
|
||||
@ -22,38 +34,88 @@ export default class SendTestEmail extends BaseCommand {
|
||||
@args.string({ description: 'Adresse destinataire' })
|
||||
declare to: string
|
||||
|
||||
@flags.string({
|
||||
description: 'Template à envoyer : checkin (default) | relance | plain',
|
||||
default: 'checkin',
|
||||
})
|
||||
declare template: string
|
||||
|
||||
@flags.string({ description: 'Adresse de reply-to (optionnelle)' })
|
||||
declare replyTo?: string
|
||||
|
||||
async run() {
|
||||
const driver = env.get('MAIL_DRIVER', 'smtp')
|
||||
const fromAddress = env.get('MAIL_FROM_ADDRESS', 'relances@rubis-sur-l-ongle.fr')
|
||||
const fromName = env.get('MAIL_FROM_NAME', "Rubis Sur l'Ongle")
|
||||
const fromName = env.get('MAIL_FROM_NAME', "Rubis sur l'ongle")
|
||||
const landingUrl = env.get('LANDING_URL', 'https://rubis.arthurbarre.fr')
|
||||
|
||||
this.logger.info(`Driver: ${driver}`)
|
||||
this.logger.info(`From: ${fromName} <${fromAddress}>`)
|
||||
this.logger.info(`To: ${this.to}`)
|
||||
if (this.replyTo) this.logger.info(`ReplyTo: ${this.replyTo}`)
|
||||
this.logger.info(`Driver: ${driver}`)
|
||||
this.logger.info(`From: ${fromName} <${fromAddress}>`)
|
||||
this.logger.info(`To: ${this.to}`)
|
||||
this.logger.info(`Template: ${this.template}`)
|
||||
if (this.replyTo) this.logger.info(`ReplyTo: ${this.replyTo}`)
|
||||
|
||||
// Données factices pour le rendu — évite d'avoir à créer une vraie
|
||||
// facture en DB juste pour tester l'email.
|
||||
const fakeInvoice = {
|
||||
numero: 'F2026-TEST',
|
||||
amountFormatted: '1 234,56 €',
|
||||
dueDateFormatted: DateTime.utc().toFormat('dd/LL/yyyy'),
|
||||
}
|
||||
|
||||
let subject: string
|
||||
let text: string
|
||||
let html: string | undefined
|
||||
|
||||
if (this.template === 'plain') {
|
||||
subject = "[Rubis] Test d'envoi (plain text)"
|
||||
text =
|
||||
`Bonjour,\n\n` +
|
||||
`Ceci est un email de test envoyé depuis Rubis sur l'ongle.\n` +
|
||||
`Driver : ${driver}\n` +
|
||||
`Date : ${new Date().toISOString()}\n\n` +
|
||||
`— L'équipe Rubis`
|
||||
html = undefined
|
||||
} else if (this.template === 'relance') {
|
||||
subject = `Facture ${fakeInvoice.numero} échue (test)`
|
||||
text =
|
||||
`Bonjour,\n\nPetit rappel pour la facture ${fakeInvoice.numero}.\n\nCordialement,\nArthur Barré`
|
||||
html = await render(
|
||||
RelanceEmail({
|
||||
brandName: 'Arthur Barré (test)',
|
||||
invoice: { ...fakeInvoice, daysLate: 3 },
|
||||
bodyText: text,
|
||||
landingUrl,
|
||||
})
|
||||
)
|
||||
} else {
|
||||
// checkin par défaut
|
||||
subject = `[Rubis] Test — Facture ${fakeInvoice.numero}, payée ?`
|
||||
text =
|
||||
`Test d'envoi du template check-in.\n\n` +
|
||||
`Lien "payée" : https://example.com/paid\n` +
|
||||
`Lien "pending" : https://example.com/pending\n\n` +
|
||||
`— L'équipe Rubis`
|
||||
html = await render(
|
||||
CheckinEmail({
|
||||
invoice: fakeInvoice,
|
||||
client: { name: 'Boulangerie Test' },
|
||||
user: { fullName: 'Arthur Barré' },
|
||||
paidUrl: 'https://example.com/paid',
|
||||
pendingUrl: 'https://example.com/pending',
|
||||
landingUrl,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const mailer = mail.use(driver)
|
||||
const response = await mailer.send((m) => {
|
||||
m.from(fromAddress, fromName)
|
||||
.to(this.to)
|
||||
.subject('[Rubis] Test d\'envoi via Resend')
|
||||
.text(
|
||||
`Bonjour,\n\n` +
|
||||
`Ceci est un email de test envoyé depuis Rubis Sur l'Ongle.\n` +
|
||||
`Si vous recevez ce message, la conf Resend (SPF/DKIM/API key) est OK.\n\n` +
|
||||
`Driver utilisé : ${driver}\n` +
|
||||
`Date : ${new Date().toISOString()}\n\n` +
|
||||
`— L'équipe Rubis`
|
||||
)
|
||||
m.from(fromAddress, fromName).to(this.to).subject(subject).text(text)
|
||||
if (html) m.html(html)
|
||||
if (this.replyTo) m.replyTo(this.replyTo)
|
||||
})
|
||||
|
||||
this.logger.success('Email envoyé')
|
||||
// Resend renvoie un messageId dans la réponse — utile pour retrouver
|
||||
// le log dans le dashboard.
|
||||
if (response?.messageId) {
|
||||
this.logger.info(`messageId: ${response.messageId}`)
|
||||
}
|
||||
|
||||
@ -7,14 +7,14 @@ const mailConfig = defineConfig({
|
||||
|
||||
from: {
|
||||
address: env.get('MAIL_FROM_ADDRESS', 'relances@rubis-sur-l-ongle.fr'),
|
||||
name: env.get('MAIL_FROM_NAME', "Rubis Sur l'Ongle"),
|
||||
name: env.get('MAIL_FROM_NAME', "Rubis sur l'ongle"),
|
||||
},
|
||||
|
||||
/**
|
||||
* Variables partagées par tous les templates Edge (logo, URL de base…).
|
||||
*/
|
||||
globals: {
|
||||
brandName: "Rubis Sur l'Ongle",
|
||||
brandName: "Rubis sur l'ongle",
|
||||
appUrl: env.get('APP_URL'),
|
||||
},
|
||||
|
||||
|
||||
@ -465,7 +465,7 @@ function Footer({ iban }: { iban: string }) {
|
||||
Règlement par virement IBAN {iban} · Pénalités de retard : 3× taux légal
|
||||
· Indemnité forfaitaire 40 € (art. L.441-10 C. com.)
|
||||
</Text>
|
||||
<Text style={styles.footerBrand}>Émise via Rubis Sur l'Ongle</Text>
|
||||
<Text style={styles.footerBrand}>Émise via Rubis sur l'ongle</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@ -75,6 +75,8 @@
|
||||
"@aws-sdk/client-s3": "^3.1043.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.1043.0",
|
||||
"@japa/api-client": "^3.2.1",
|
||||
"@react-email/components": "^1.0.12",
|
||||
"@react-email/render": "^2.0.8",
|
||||
"@react-pdf/renderer": "^4.5.1",
|
||||
"@tuyau/core": "^1.2.2",
|
||||
"@vinejs/vine": "^4.3.1",
|
||||
|
||||
@ -68,6 +68,9 @@ export default await Env.create(new URL('../', import.meta.url), {
|
||||
// Web (URL du SPA pour redirects post-checkin)
|
||||
WEB_URL: Env.schema.string.optional({ format: 'url', tld: false }),
|
||||
|
||||
// Landing public (lien dans le footer des emails — branding)
|
||||
LANDING_URL: Env.schema.string.optional({ format: 'url', tld: false }),
|
||||
|
||||
// Auth
|
||||
ACCESS_TOKEN_TTL_MINUTES: Env.schema.number.optional(),
|
||||
REFRESH_TOKEN_TTL_DAYS: Env.schema.number.optional(),
|
||||
|
||||
428
pnpm-lock.yaml
generated
428
pnpm-lock.yaml
generated
@ -77,6 +77,12 @@ importers:
|
||||
'@japa/api-client':
|
||||
specifier: ^3.2.1
|
||||
version: 3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0)
|
||||
'@react-email/components':
|
||||
specifier: ^1.0.12
|
||||
version: 1.0.12(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
'@react-email/render':
|
||||
specifier: ^2.0.8
|
||||
version: 2.0.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
'@react-pdf/renderer':
|
||||
specifier: ^4.5.1
|
||||
version: 4.5.1(react@19.2.5)
|
||||
@ -1953,6 +1959,192 @@ packages:
|
||||
'@radix-ui/rect@1.1.1':
|
||||
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
|
||||
|
||||
'@react-email/body@0.3.0':
|
||||
resolution: {integrity: sha512-uGo0BOOzjbMUo3lu+BIDWayvn5o6Xyfmnlla5VGf05n8gHMvO1ll7U4FtzWe3hxMLwt53pmc4iE0M+B5slG+Ug==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/button@0.2.1':
|
||||
resolution: {integrity: sha512-qXyj7RZLE7POy9BMKSoqQ00tOXThjOZSUnI2Yu9i29IHngPlmrNayIWBoVKtElES7OWwypUcpiajwi1mUWx6/A==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/code-block@0.2.1':
|
||||
resolution: {integrity: sha512-M3B7JpVH4ytgn83/ujRR1k1DQHvTeABiDM61OvAbjLRPhC/5KLHU5KkzIbbuGIrjWwxAbL1kSQzU8MhLEtSxyw==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/code-inline@0.0.6':
|
||||
resolution: {integrity: sha512-jfhebvv3dVsp3OdPgKXnk8+e2pBiDVZejDOBFzBa/IblrAJ9cQDkN6rBD5IyEg8hTOxwbw3iaI/yZFmDmIguIA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/column@0.0.14':
|
||||
resolution: {integrity: sha512-f+W+Bk2AjNO77zynE33rHuQhyqVICx4RYtGX9NKsGUg0wWjdGP0qAuIkhx9Rnmk4/hFMo1fUrtYNqca9fwJdHg==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/components@1.0.12':
|
||||
resolution: {integrity: sha512-tH18JhPDWgE+3jnYkzyB6ZrZdfNnEsFe4PwmuXmlOw4NGIysP8wPY5aXZg++pTG9qUabXg1nzX/FGHGkObH8xQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/container@0.0.16':
|
||||
resolution: {integrity: sha512-QWBB56RkkU0AJ9h+qy33gfT5iuZknPC7Un/IjZv9B0QmMIK+WWacc0cH6y2SV5Cv/b99hU94fjEMOOO4enpkbQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/font@0.0.10':
|
||||
resolution: {integrity: sha512-0urVSgCmQIfx5r7Xc586miBnQUVnGp3OTYUm8m5pwtQRdTRO5XrTtEfNJ3JhYhSOruV0nD8fd+dXtKXobum6tA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/head@0.0.13':
|
||||
resolution: {integrity: sha512-AJg6le/08Gz4tm+6MtKXqtNNyKHzmooOCdmtqmWxD7FxoAdU1eVcizhtQ0gcnVaY6ethEyE/hnEzQxt1zu5Kog==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/heading@0.0.16':
|
||||
resolution: {integrity: sha512-jmsKnQm1ykpBzw4hCYHwBkt5pW2jScXffPeEH5ZRF5tZeF5b1pvlFTO9han7C0pCkZYo1kEvWiRtx69yfCIwuw==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/hr@0.0.12':
|
||||
resolution: {integrity: sha512-TwmOmBDibavUQpXBxpmZYi2Iks/yeZOzFYh+di9EltMSnEabH8dMZXrl+pxNXzCgZ2XE8HY7VmUL65Lenfu5PA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/html@0.0.12':
|
||||
resolution: {integrity: sha512-KTShZesan+UsreU7PDUV90afrZwU5TLwYlALuCSU0OT+/U8lULNNbAUekg+tGwCnOfIKYtpDPKkAMRdYlqUznw==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/img@0.0.12':
|
||||
resolution: {integrity: sha512-sRCpEARNVTf3FQhZOC+JTvu5r6ubiYWkT0ucYXg8ctkyi4G8QG+jgYPiNUqVeTLA2STOfmPM/nrk1nb84y6CPQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/link@0.0.13':
|
||||
resolution: {integrity: sha512-lkWc/NjOcefRZMkQoSDDbuKBEBDES9aXnFEOuPH845wD3TxPwh+QTf0fStuzjoRLUZWpHnio4z7qGGRYusn/sw==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/markdown@0.0.18':
|
||||
resolution: {integrity: sha512-gSuYK5fsMbGk87jDebqQ6fa2fKcWlkf2Dkva8kMONqLgGCq8/0d+ZQYMEJsdidIeBo3kmsnHZPrwdFB4HgjUXg==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/preview@0.0.14':
|
||||
resolution: {integrity: sha512-aYK8q0IPkBXyMsbpMXgxazwHxYJxTrXrV95GFuu2HbEiIToMwSyUgb8HDFYwPqqfV03/jbwqlsXmFxsOd+VNaw==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/render@2.0.6':
|
||||
resolution: {integrity: sha512-xOzaYkH3jLZKqN5MqrTXYnmqBYUnZSVbkxdb5PGGmDcK6sKDVMliaDiSwfXajRC9JtSHTcGc2tmGLHWuCgVpog==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/render@2.0.8':
|
||||
resolution: {integrity: sha512-5udvVr3U/WuGJZfLdLBOhkzrqRWd2Q5ZYmF7ppcy7FzWcwgshdqLMNqJOXcVzAXJXg/2bm7D+WGJzTtZOZMQnQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/row@0.0.13':
|
||||
resolution: {integrity: sha512-bYnOac40vIKCId7IkwuLAAsa3fKfSfqCvv6epJKmPE0JBuu5qI4FHFCl9o9dVpIIS08s/ub+Y/txoMt0dYziGw==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/section@0.0.17':
|
||||
resolution: {integrity: sha512-qNl65ye3W0Rd5udhdORzTV9ezjb+GFqQQSae03NDzXtmJq6sqVXNWNiVolAjvJNypim+zGXmv6J9TcV5aNtE/w==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/tailwind@2.0.7':
|
||||
resolution: {integrity: sha512-kGw80weVFXikcnCXbigTGXGWQ0MRCSYNCudcdkWxebkWYd0FG6/NPoN3V1p/u68/4+NxZwYPVi2fhnp0x23HdA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
'@react-email/body': '>=0'
|
||||
'@react-email/button': '>=0'
|
||||
'@react-email/code-block': '>=0'
|
||||
'@react-email/code-inline': '>=0'
|
||||
'@react-email/container': '>=0'
|
||||
'@react-email/heading': '>=0'
|
||||
'@react-email/hr': '>=0'
|
||||
'@react-email/img': '>=0'
|
||||
'@react-email/link': '>=0'
|
||||
'@react-email/preview': '>=0'
|
||||
'@react-email/text': '>=0'
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@react-email/body':
|
||||
optional: true
|
||||
'@react-email/button':
|
||||
optional: true
|
||||
'@react-email/code-block':
|
||||
optional: true
|
||||
'@react-email/code-inline':
|
||||
optional: true
|
||||
'@react-email/container':
|
||||
optional: true
|
||||
'@react-email/heading':
|
||||
optional: true
|
||||
'@react-email/hr':
|
||||
optional: true
|
||||
'@react-email/img':
|
||||
optional: true
|
||||
'@react-email/link':
|
||||
optional: true
|
||||
'@react-email/preview':
|
||||
optional: true
|
||||
|
||||
'@react-email/text@0.1.6':
|
||||
resolution: {integrity: sha512-TYqkioRS45wTR5il3dYk/SbUjjEdhSwh9BtRNB99qNH1pXAwA45H7rAuxehiu8iJQJH0IyIr+6n62gBz9ezmsw==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-pdf/fns@3.1.3':
|
||||
resolution: {integrity: sha512-0I7pApDr1/RLAKbizuLy/IHTEa93LSPy/bEwYniboC3Xqnp6Od8xFJKbKEzGw2wh/5zKFFwl00g4t9RwgIMc3w==}
|
||||
|
||||
@ -2306,6 +2498,9 @@ packages:
|
||||
'@sec-ant/readable-stream@0.4.1':
|
||||
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
|
||||
|
||||
'@selderee/plugin-htmlparser2@0.11.0':
|
||||
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
|
||||
|
||||
'@sinclair/typebox@0.34.49':
|
||||
resolution: {integrity: sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==}
|
||||
|
||||
@ -3701,6 +3896,19 @@ packages:
|
||||
dom-accessibility-api@0.6.3:
|
||||
resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==}
|
||||
|
||||
dom-serializer@2.0.0:
|
||||
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
|
||||
|
||||
domelementtype@2.3.0:
|
||||
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
|
||||
|
||||
domhandler@5.0.3:
|
||||
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
domutils@3.2.2:
|
||||
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -3742,6 +3950,10 @@ packages:
|
||||
resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
entities@4.5.0:
|
||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||
engines: {node: '>=0.12'}
|
||||
|
||||
entities@6.0.1:
|
||||
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
|
||||
engines: {node: '>=0.12'}
|
||||
@ -4212,6 +4424,13 @@ packages:
|
||||
resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
html-to-text@9.0.5:
|
||||
resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
htmlparser2@8.0.2:
|
||||
resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
|
||||
|
||||
http-errors@2.0.1:
|
||||
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@ -4577,6 +4796,9 @@ packages:
|
||||
resolution: {integrity: sha512-9zy9lkjac+TR1c2tG+mkNSVlyOpInnWdSMiue4F+kq8TwJSgv6o8jhLRg8Ho6SnZ9wOYUq/yozts9qQCfk7bIw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
leac@0.6.0:
|
||||
resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==}
|
||||
|
||||
levn@0.4.1:
|
||||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@ -4727,6 +4949,11 @@ packages:
|
||||
magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
marked@15.0.12:
|
||||
resolution: {integrity: sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==}
|
||||
engines: {node: '>= 18'}
|
||||
hasBin: true
|
||||
|
||||
math-intrinsics@1.1.0:
|
||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -4965,6 +5192,9 @@ packages:
|
||||
parse5@7.3.0:
|
||||
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
|
||||
|
||||
parseley@0.12.1:
|
||||
resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==}
|
||||
|
||||
parseurl@1.3.3:
|
||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@ -5001,6 +5231,9 @@ packages:
|
||||
resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
|
||||
engines: {node: '>= 14.16'}
|
||||
|
||||
peberminta@0.9.0:
|
||||
resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==}
|
||||
|
||||
pg-cloudflare@1.3.0:
|
||||
resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==}
|
||||
|
||||
@ -5144,6 +5377,10 @@ packages:
|
||||
resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
prismjs@1.30.0:
|
||||
resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
process-warning@5.0.0:
|
||||
resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==}
|
||||
|
||||
@ -5417,6 +5654,9 @@ packages:
|
||||
secure-json-parse@4.1.0:
|
||||
resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==}
|
||||
|
||||
selderee@0.11.0:
|
||||
resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==}
|
||||
|
||||
semver@6.3.1:
|
||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||
hasBin: true
|
||||
@ -8026,6 +8266,137 @@ snapshots:
|
||||
|
||||
'@radix-ui/rect@1.1.1': {}
|
||||
|
||||
'@react-email/body@0.3.0(react@19.2.5)':
|
||||
dependencies:
|
||||
react: 19.2.5
|
||||
|
||||
'@react-email/button@0.2.1(react@19.2.5)':
|
||||
dependencies:
|
||||
react: 19.2.5
|
||||
|
||||
'@react-email/code-block@0.2.1(react@19.2.5)':
|
||||
dependencies:
|
||||
prismjs: 1.30.0
|
||||
react: 19.2.5
|
||||
|
||||
'@react-email/code-inline@0.0.6(react@19.2.5)':
|
||||
dependencies:
|
||||
react: 19.2.5
|
||||
|
||||
'@react-email/column@0.0.14(react@19.2.5)':
|
||||
dependencies:
|
||||
react: 19.2.5
|
||||
|
||||
'@react-email/components@1.0.12(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
|
||||
dependencies:
|
||||
'@react-email/body': 0.3.0(react@19.2.5)
|
||||
'@react-email/button': 0.2.1(react@19.2.5)
|
||||
'@react-email/code-block': 0.2.1(react@19.2.5)
|
||||
'@react-email/code-inline': 0.0.6(react@19.2.5)
|
||||
'@react-email/column': 0.0.14(react@19.2.5)
|
||||
'@react-email/container': 0.0.16(react@19.2.5)
|
||||
'@react-email/font': 0.0.10(react@19.2.5)
|
||||
'@react-email/head': 0.0.13(react@19.2.5)
|
||||
'@react-email/heading': 0.0.16(react@19.2.5)
|
||||
'@react-email/hr': 0.0.12(react@19.2.5)
|
||||
'@react-email/html': 0.0.12(react@19.2.5)
|
||||
'@react-email/img': 0.0.12(react@19.2.5)
|
||||
'@react-email/link': 0.0.13(react@19.2.5)
|
||||
'@react-email/markdown': 0.0.18(react@19.2.5)
|
||||
'@react-email/preview': 0.0.14(react@19.2.5)
|
||||
'@react-email/render': 2.0.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
'@react-email/row': 0.0.13(react@19.2.5)
|
||||
'@react-email/section': 0.0.17(react@19.2.5)
|
||||
'@react-email/tailwind': 2.0.7(@react-email/body@0.3.0(react@19.2.5))(@react-email/button@0.2.1(react@19.2.5))(@react-email/code-block@0.2.1(react@19.2.5))(@react-email/code-inline@0.0.6(react@19.2.5))(@react-email/container@0.0.16(react@19.2.5))(@react-email/heading@0.0.16(react@19.2.5))(@react-email/hr@0.0.12(react@19.2.5))(@react-email/img@0.0.12(react@19.2.5))(@react-email/link@0.0.13(react@19.2.5))(@react-email/preview@0.0.14(react@19.2.5))(@react-email/text@0.1.6(react@19.2.5))(react@19.2.5)
|
||||
'@react-email/text': 0.1.6(react@19.2.5)
|
||||
react: 19.2.5
|
||||
transitivePeerDependencies:
|
||||
- react-dom
|
||||
|
||||
'@react-email/container@0.0.16(react@19.2.5)':
|
||||
dependencies:
|
||||
react: 19.2.5
|
||||
|
||||
'@react-email/font@0.0.10(react@19.2.5)':
|
||||
dependencies:
|
||||
react: 19.2.5
|
||||
|
||||
'@react-email/head@0.0.13(react@19.2.5)':
|
||||
dependencies:
|
||||
react: 19.2.5
|
||||
|
||||
'@react-email/heading@0.0.16(react@19.2.5)':
|
||||
dependencies:
|
||||
react: 19.2.5
|
||||
|
||||
'@react-email/hr@0.0.12(react@19.2.5)':
|
||||
dependencies:
|
||||
react: 19.2.5
|
||||
|
||||
'@react-email/html@0.0.12(react@19.2.5)':
|
||||
dependencies:
|
||||
react: 19.2.5
|
||||
|
||||
'@react-email/img@0.0.12(react@19.2.5)':
|
||||
dependencies:
|
||||
react: 19.2.5
|
||||
|
||||
'@react-email/link@0.0.13(react@19.2.5)':
|
||||
dependencies:
|
||||
react: 19.2.5
|
||||
|
||||
'@react-email/markdown@0.0.18(react@19.2.5)':
|
||||
dependencies:
|
||||
marked: 15.0.12
|
||||
react: 19.2.5
|
||||
|
||||
'@react-email/preview@0.0.14(react@19.2.5)':
|
||||
dependencies:
|
||||
react: 19.2.5
|
||||
|
||||
'@react-email/render@2.0.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
|
||||
dependencies:
|
||||
html-to-text: 9.0.5
|
||||
prettier: 3.8.3
|
||||
react: 19.2.5
|
||||
react-dom: 19.2.5(react@19.2.5)
|
||||
|
||||
'@react-email/render@2.0.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
|
||||
dependencies:
|
||||
html-to-text: 9.0.5
|
||||
prettier: 3.8.3
|
||||
react: 19.2.5
|
||||
react-dom: 19.2.5(react@19.2.5)
|
||||
|
||||
'@react-email/row@0.0.13(react@19.2.5)':
|
||||
dependencies:
|
||||
react: 19.2.5
|
||||
|
||||
'@react-email/section@0.0.17(react@19.2.5)':
|
||||
dependencies:
|
||||
react: 19.2.5
|
||||
|
||||
'@react-email/tailwind@2.0.7(@react-email/body@0.3.0(react@19.2.5))(@react-email/button@0.2.1(react@19.2.5))(@react-email/code-block@0.2.1(react@19.2.5))(@react-email/code-inline@0.0.6(react@19.2.5))(@react-email/container@0.0.16(react@19.2.5))(@react-email/heading@0.0.16(react@19.2.5))(@react-email/hr@0.0.12(react@19.2.5))(@react-email/img@0.0.12(react@19.2.5))(@react-email/link@0.0.13(react@19.2.5))(@react-email/preview@0.0.14(react@19.2.5))(@react-email/text@0.1.6(react@19.2.5))(react@19.2.5)':
|
||||
dependencies:
|
||||
'@react-email/text': 0.1.6(react@19.2.5)
|
||||
react: 19.2.5
|
||||
tailwindcss: 4.2.4
|
||||
optionalDependencies:
|
||||
'@react-email/body': 0.3.0(react@19.2.5)
|
||||
'@react-email/button': 0.2.1(react@19.2.5)
|
||||
'@react-email/code-block': 0.2.1(react@19.2.5)
|
||||
'@react-email/code-inline': 0.0.6(react@19.2.5)
|
||||
'@react-email/container': 0.0.16(react@19.2.5)
|
||||
'@react-email/heading': 0.0.16(react@19.2.5)
|
||||
'@react-email/hr': 0.0.12(react@19.2.5)
|
||||
'@react-email/img': 0.0.12(react@19.2.5)
|
||||
'@react-email/link': 0.0.13(react@19.2.5)
|
||||
'@react-email/preview': 0.0.14(react@19.2.5)
|
||||
|
||||
'@react-email/text@0.1.6(react@19.2.5)':
|
||||
dependencies:
|
||||
react: 19.2.5
|
||||
|
||||
'@react-pdf/fns@3.1.3': {}
|
||||
|
||||
'@react-pdf/font@4.0.8':
|
||||
@ -8323,6 +8694,11 @@ snapshots:
|
||||
|
||||
'@sec-ant/readable-stream@0.4.1': {}
|
||||
|
||||
'@selderee/plugin-htmlparser2@0.11.0':
|
||||
dependencies:
|
||||
domhandler: 5.0.3
|
||||
selderee: 0.11.0
|
||||
|
||||
'@sinclair/typebox@0.34.49': {}
|
||||
|
||||
'@sindresorhus/is@7.2.0': {}
|
||||
@ -9757,6 +10133,24 @@ snapshots:
|
||||
|
||||
dom-accessibility-api@0.6.3: {}
|
||||
|
||||
dom-serializer@2.0.0:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 5.0.3
|
||||
entities: 4.5.0
|
||||
|
||||
domelementtype@2.3.0: {}
|
||||
|
||||
domhandler@5.0.3:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
|
||||
domutils@3.2.2:
|
||||
dependencies:
|
||||
dom-serializer: 2.0.0
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 5.0.3
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
@ -9795,6 +10189,8 @@ snapshots:
|
||||
ansi-colors: 4.1.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
entities@4.5.0: {}
|
||||
|
||||
entities@6.0.1: {}
|
||||
|
||||
environment@1.1.0: {}
|
||||
@ -10332,6 +10728,21 @@ snapshots:
|
||||
dependencies:
|
||||
whatwg-encoding: 3.1.1
|
||||
|
||||
html-to-text@9.0.5:
|
||||
dependencies:
|
||||
'@selderee/plugin-htmlparser2': 0.11.0
|
||||
deepmerge: 4.3.1
|
||||
dom-serializer: 2.0.0
|
||||
htmlparser2: 8.0.2
|
||||
selderee: 0.11.0
|
||||
|
||||
htmlparser2@8.0.2:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 5.0.3
|
||||
domutils: 3.2.2
|
||||
entities: 4.5.0
|
||||
|
||||
http-errors@2.0.1:
|
||||
dependencies:
|
||||
depd: 2.0.0
|
||||
@ -10621,6 +11032,8 @@ snapshots:
|
||||
|
||||
ky@1.14.3: {}
|
||||
|
||||
leac@0.6.0: {}
|
||||
|
||||
levn@0.4.1:
|
||||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
@ -10762,6 +11175,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
marked@15.0.12: {}
|
||||
|
||||
math-intrinsics@1.1.0: {}
|
||||
|
||||
media-engine@1.0.3: {}
|
||||
@ -10987,6 +11402,11 @@ snapshots:
|
||||
dependencies:
|
||||
entities: 6.0.1
|
||||
|
||||
parseley@0.12.1:
|
||||
dependencies:
|
||||
leac: 0.6.0
|
||||
peberminta: 0.9.0
|
||||
|
||||
parseurl@1.3.3: {}
|
||||
|
||||
path-browserify@1.0.1: {}
|
||||
@ -11007,6 +11427,8 @@ snapshots:
|
||||
|
||||
pathval@2.0.1: {}
|
||||
|
||||
peberminta@0.9.0: {}
|
||||
|
||||
pg-cloudflare@1.3.0:
|
||||
optional: true
|
||||
|
||||
@ -11166,6 +11588,8 @@ snapshots:
|
||||
dependencies:
|
||||
parse-ms: 4.0.0
|
||||
|
||||
prismjs@1.30.0: {}
|
||||
|
||||
process-warning@5.0.0: {}
|
||||
|
||||
prop-types@15.8.1:
|
||||
@ -11458,6 +11882,10 @@ snapshots:
|
||||
|
||||
secure-json-parse@4.1.0: {}
|
||||
|
||||
selderee@0.11.0:
|
||||
dependencies:
|
||||
parseley: 0.12.1
|
||||
|
||||
semver@6.3.1: {}
|
||||
|
||||
semver@7.7.4: {}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user