@vennyx/mila-react-template 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/button.d.ts +55 -0
- package/dist/button.js +84 -0
- package/dist/button.js.map +1 -0
- package/dist/divider.d.ts +5 -0
- package/dist/divider.js +7 -0
- package/dist/divider.js.map +1 -0
- package/dist/footer.d.ts +15 -0
- package/dist/footer.js +28 -0
- package/dist/footer.js.map +1 -0
- package/dist/head.d.ts +27 -0
- package/dist/head.js +58 -0
- package/dist/head.js.map +1 -0
- package/dist/html.d.ts +32 -0
- package/dist/html.js +29 -0
- package/dist/html.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/layout.d.ts +23 -0
- package/dist/layout.js +26 -0
- package/dist/layout.js.map +1 -0
- package/dist/lint.d.ts +11 -0
- package/dist/lint.js +58 -0
- package/dist/lint.js.map +1 -0
- package/dist/logo.d.ts +27 -0
- package/dist/logo.js +34 -0
- package/dist/logo.js.map +1 -0
- package/dist/otp-code.d.ts +13 -0
- package/dist/otp-code.js +27 -0
- package/dist/otp-code.js.map +1 -0
- package/dist/render.d.ts +21 -0
- package/dist/render.js +29 -0
- package/dist/render.js.map +1 -0
- package/dist/tokens.d.ts +63 -0
- package/dist/tokens.js +64 -0
- package/dist/tokens.js.map +1 -0
- package/package.json +42 -0
package/dist/button.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type * as React from 'react';
|
|
2
|
+
export interface MilaButtonFixedProps {
|
|
3
|
+
href: string;
|
|
4
|
+
label: string;
|
|
5
|
+
/** px. Default 240 -- comfortable for a single short CTA phrase in
|
|
6
|
+
* either locale; pass a wider value for longer Turkish labels rather
|
|
7
|
+
* than switching to MilaButtonFluid, when Outlook rounded corners still
|
|
8
|
+
* matter for that specific template. */
|
|
9
|
+
width?: number;
|
|
10
|
+
/** px. Default 48. */
|
|
11
|
+
height?: number;
|
|
12
|
+
color?: string;
|
|
13
|
+
textColor?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A real, VML-rounded-corner, fixed-width bulletproof button for Outlook
|
|
17
|
+
* Desktop (2007-2021+, Word rendering engine). Verified directly from
|
|
18
|
+
* react-email@6.7.0's own source that its built-in `Button` component does
|
|
19
|
+
* NOT emit VML at all (it uses a different, non-VML MSO font-width-spacer
|
|
20
|
+
* padding technique instead -- see MilaButtonFluid below) -- so this
|
|
21
|
+
* component is fully hand-written, not a wrapper around react-email's own
|
|
22
|
+
* Button.
|
|
23
|
+
*
|
|
24
|
+
* Rendered as one `dangerouslySetInnerHTML` string rather than nested JSX:
|
|
25
|
+
* the entire markup is 100% static structure with a handful of interpolated
|
|
26
|
+
* values (href/label/dimensions/colors), and the two "and everyone else"
|
|
27
|
+
* variants (`<!--[if !mso]><!-- -->` wrapping a real `<a>`) fight React's
|
|
28
|
+
* JSX model for no benefit here -- a single literal template string is
|
|
29
|
+
* simpler and more directly inspectable against the exact bulletproof
|
|
30
|
+
* pattern research verified, with zero risk of JSX/comment-escaping
|
|
31
|
+
* surprises.
|
|
32
|
+
*
|
|
33
|
+
* See docs/superpowers/specs/2026-07-10-mila-email-engine-design.md §5 and
|
|
34
|
+
* the research doc's §1 for the exact source pattern this mirrors
|
|
35
|
+
* (Litmus/dev.to/Campaign Monitor -- v:roundrect, not v:rect, since
|
|
36
|
+
* v:roundrect supports the `arcsize` attribute for rounded corners).
|
|
37
|
+
*/
|
|
38
|
+
export declare function MilaButtonFixed({ href, label, width, height, color, textColor }: MilaButtonFixedProps): React.JSX.Element;
|
|
39
|
+
export interface MilaButtonFluidProps {
|
|
40
|
+
href: string;
|
|
41
|
+
label: string;
|
|
42
|
+
style?: React.CSSProperties;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A fluid-width button whose padding survives Outlook Desktop via
|
|
46
|
+
* react-email's own built-in MSO font-width-spacer hack (verified directly
|
|
47
|
+
* from its source: `Button` computes `mso-font-width`/`mso-text-raise`
|
|
48
|
+
* conditional-comment spacer `<i>` tags from the `padding` style prop) --
|
|
49
|
+
* no VML, so no rounded corners in Outlook Desktop specifically (square
|
|
50
|
+
* corners there only; every other client gets the real `border-radius`).
|
|
51
|
+
* Use this variant where a bilingual label's length varies too much
|
|
52
|
+
* between locales for a fixed width to look right in both (Turkish CTA
|
|
53
|
+
* strings routinely run longer than their English equivalents).
|
|
54
|
+
*/
|
|
55
|
+
export declare function MilaButtonFluid({ href, label, style }: MilaButtonFluidProps): React.JSX.Element;
|
package/dist/button.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Button } from 'react-email';
|
|
3
|
+
import { colors, fontStack } from './tokens.js';
|
|
4
|
+
function escapeHtmlText(value) {
|
|
5
|
+
return value
|
|
6
|
+
.replace(/&/g, '&')
|
|
7
|
+
.replace(/</g, '<')
|
|
8
|
+
.replace(/>/g, '>')
|
|
9
|
+
.replace(/"/g, '"')
|
|
10
|
+
.replace(/'/g, ''');
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A real, VML-rounded-corner, fixed-width bulletproof button for Outlook
|
|
14
|
+
* Desktop (2007-2021+, Word rendering engine). Verified directly from
|
|
15
|
+
* react-email@6.7.0's own source that its built-in `Button` component does
|
|
16
|
+
* NOT emit VML at all (it uses a different, non-VML MSO font-width-spacer
|
|
17
|
+
* padding technique instead -- see MilaButtonFluid below) -- so this
|
|
18
|
+
* component is fully hand-written, not a wrapper around react-email's own
|
|
19
|
+
* Button.
|
|
20
|
+
*
|
|
21
|
+
* Rendered as one `dangerouslySetInnerHTML` string rather than nested JSX:
|
|
22
|
+
* the entire markup is 100% static structure with a handful of interpolated
|
|
23
|
+
* values (href/label/dimensions/colors), and the two "and everyone else"
|
|
24
|
+
* variants (`<!--[if !mso]><!-- -->` wrapping a real `<a>`) fight React's
|
|
25
|
+
* JSX model for no benefit here -- a single literal template string is
|
|
26
|
+
* simpler and more directly inspectable against the exact bulletproof
|
|
27
|
+
* pattern research verified, with zero risk of JSX/comment-escaping
|
|
28
|
+
* surprises.
|
|
29
|
+
*
|
|
30
|
+
* See docs/superpowers/specs/2026-07-10-mila-email-engine-design.md §5 and
|
|
31
|
+
* the research doc's §1 for the exact source pattern this mirrors
|
|
32
|
+
* (Litmus/dev.to/Campaign Monitor -- v:roundrect, not v:rect, since
|
|
33
|
+
* v:roundrect supports the `arcsize` attribute for rounded corners).
|
|
34
|
+
*/
|
|
35
|
+
export function MilaButtonFixed({ href, label, width = 240, height = 48, color = colors.accent, textColor = colors.buttonText }) {
|
|
36
|
+
// Deliberately never wired to `.mila-accent-bg`: a CTA button's brand
|
|
37
|
+
// accent color is a fixed constant in both light and dark mode, not
|
|
38
|
+
// something that should fade into the surrounding background.
|
|
39
|
+
const safeLabel = escapeHtmlText(label);
|
|
40
|
+
const safeHref = escapeHtmlText(href);
|
|
41
|
+
const safeColor = escapeHtmlText(color);
|
|
42
|
+
const safeTextColor = escapeHtmlText(textColor);
|
|
43
|
+
const html = `<table role="presentation" border="0" cellspacing="0" cellpadding="0" align="center">
|
|
44
|
+
<tr>
|
|
45
|
+
<td align="center">
|
|
46
|
+
<!--[if mso]>
|
|
47
|
+
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="${safeHref}" style="height:${height}px;v-text-anchor:middle;width:${width}px;" arcsize="12%" fillcolor="${safeColor}" strokecolor="${safeColor}">
|
|
48
|
+
<w:anchorlock/>
|
|
49
|
+
<center style="color:${safeTextColor};font-family:${fontStack.body};font-size:16px;font-weight:bold;">${safeLabel}</center>
|
|
50
|
+
</v:roundrect>
|
|
51
|
+
<![endif]-->
|
|
52
|
+
<!--[if !mso]><!-- -->
|
|
53
|
+
<a href="${safeHref}" target="_blank" role="button" style="background-color:${safeColor};border:1px solid ${safeColor};border-radius:6px;color:${safeTextColor};display:inline-block;font-family:${fontStack.body};font-size:16px;font-weight:bold;line-height:${height}px;text-align:center;text-decoration:none;width:${width}px;-webkit-text-size-adjust:none;mso-hide:all;">${safeLabel}</a>
|
|
54
|
+
<!--<![endif]-->
|
|
55
|
+
</td>
|
|
56
|
+
</tr>
|
|
57
|
+
</table>`;
|
|
58
|
+
return _jsx("div", { dangerouslySetInnerHTML: { __html: html } });
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A fluid-width button whose padding survives Outlook Desktop via
|
|
62
|
+
* react-email's own built-in MSO font-width-spacer hack (verified directly
|
|
63
|
+
* from its source: `Button` computes `mso-font-width`/`mso-text-raise`
|
|
64
|
+
* conditional-comment spacer `<i>` tags from the `padding` style prop) --
|
|
65
|
+
* no VML, so no rounded corners in Outlook Desktop specifically (square
|
|
66
|
+
* corners there only; every other client gets the real `border-radius`).
|
|
67
|
+
* Use this variant where a bilingual label's length varies too much
|
|
68
|
+
* between locales for a fixed width to look right in both (Turkish CTA
|
|
69
|
+
* strings routinely run longer than their English equivalents).
|
|
70
|
+
*/
|
|
71
|
+
export function MilaButtonFluid({ href, label, style }) {
|
|
72
|
+
return (_jsx(Button, { href: href, style: {
|
|
73
|
+
backgroundColor: colors.accent,
|
|
74
|
+
color: colors.buttonText,
|
|
75
|
+
fontFamily: fontStack.body,
|
|
76
|
+
fontSize: 16,
|
|
77
|
+
fontWeight: 'bold',
|
|
78
|
+
textDecoration: 'none',
|
|
79
|
+
borderRadius: 6,
|
|
80
|
+
padding: '14px 24px',
|
|
81
|
+
...style,
|
|
82
|
+
}, children: label }));
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=button.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"button.js","sourceRoot":"","sources":["../src/button.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAEhD,SAAS,cAAc,CAAC,KAAa;IACnC,OAAO,KAAK;SACT,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;AAC5B,CAAC;AAgBD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,eAAe,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,GAAG,GAAG,EAAE,MAAM,GAAG,EAAE,EAAE,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAAC,UAAU,EAAwB;IACnJ,sEAAsE;IACtE,oEAAoE;IACpE,8DAA8D;IAC9D,MAAM,SAAS,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IACxC,MAAM,QAAQ,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;IACtC,MAAM,SAAS,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IACxC,MAAM,aAAa,GAAG,cAAc,CAAC,SAAS,CAAC,CAAC;IAChD,MAAM,IAAI,GAAG;;;;mHAIoG,QAAQ,mBAAmB,MAAM,iCAAiC,KAAK,iCAAiC,SAAS,kBAAkB,SAAS;;+BAEhO,aAAa,gBAAgB,SAAS,CAAC,IAAI,sCAAsC,SAAS;;;;iBAIxG,QAAQ,2DAA2D,SAAS,qBAAqB,SAAS,4BAA4B,aAAa,qCAAqC,SAAS,CAAC,IAAI,gDAAgD,MAAM,mDAAmD,KAAK,mDAAmD,SAAS;;;;SAIxX,CAAC;IACR,OAAO,cAAK,uBAAuB,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,GAAI,CAAC;AAC5D,CAAC;AAQD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,eAAe,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAwB;IAC1E,OAAO,CACL,KAAC,MAAM,IACL,IAAI,EAAE,IAAI,EACV,KAAK,EAAE;YACL,eAAe,EAAE,MAAM,CAAC,MAAM;YAC9B,KAAK,EAAE,MAAM,CAAC,UAAU;YACxB,UAAU,EAAE,SAAS,CAAC,IAAI;YAC1B,QAAQ,EAAE,EAAE;YACZ,UAAU,EAAE,MAAM;YAClB,cAAc,EAAE,MAAM;YACtB,YAAY,EAAE,CAAC;YACf,OAAO,EAAE,WAAW;YACpB,GAAG,KAAK;SACT,YAEA,KAAK,GACC,CACV,CAAC;AACJ,CAAC"}
|
package/dist/divider.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Hr } from 'react-email';
|
|
3
|
+
import { colors } from './tokens.js';
|
|
4
|
+
export function MilaDivider({ style }) {
|
|
5
|
+
return _jsx(Hr, { className: "mila-border", style: { borderColor: colors.border, margin: '24px 0', ...style } });
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=divider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"divider.js","sourceRoot":"","sources":["../src/divider.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAMrC,MAAM,UAAU,WAAW,CAAC,EAAE,KAAK,EAAoB;IACrD,OAAO,KAAC,EAAE,IAAC,SAAS,EAAC,aAAa,EAAC,KAAK,EAAE,EAAE,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,KAAK,EAAE,GAAI,CAAC;AAC3G,CAAC"}
|
package/dist/footer.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The only footer variant shipped in v1 -- deliberately excludes a postal
|
|
3
|
+
* address and an unsubscribe link. Neither is legally required for
|
|
4
|
+
* transactional mail (CAN-SPAM's own transactional-message exemption names
|
|
5
|
+
* password resets/receipts/OTPs explicitly; GDPR mandates no specific
|
|
6
|
+
* footer content at all), and an unsubscribe link on a password-reset/
|
|
7
|
+
* account-security email is itself a UX/security anti-pattern -- it
|
|
8
|
+
* implies account-security mail is something a user can opt out of. A
|
|
9
|
+
* distinct `MilaMarketingFooter` (postal address, unsubscribe, İYS
|
|
10
|
+
* consent-hook for Turkish recipients) is explicit future scope, gated
|
|
11
|
+
* behind whichever future phase adds bulk/marketing send capability --
|
|
12
|
+
* not this one. See
|
|
13
|
+
* docs/superpowers/specs/2026-07-10-mila-email-engine-design.md §6.3/§11.
|
|
14
|
+
*/
|
|
15
|
+
export declare function MilaFooter(): import("react").JSX.Element;
|
package/dist/footer.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { colors, fontStack, spacing } from './tokens.js';
|
|
3
|
+
/**
|
|
4
|
+
* The only footer variant shipped in v1 -- deliberately excludes a postal
|
|
5
|
+
* address and an unsubscribe link. Neither is legally required for
|
|
6
|
+
* transactional mail (CAN-SPAM's own transactional-message exemption names
|
|
7
|
+
* password resets/receipts/OTPs explicitly; GDPR mandates no specific
|
|
8
|
+
* footer content at all), and an unsubscribe link on a password-reset/
|
|
9
|
+
* account-security email is itself a UX/security anti-pattern -- it
|
|
10
|
+
* implies account-security mail is something a user can opt out of. A
|
|
11
|
+
* distinct `MilaMarketingFooter` (postal address, unsubscribe, İYS
|
|
12
|
+
* consent-hook for Turkish recipients) is explicit future scope, gated
|
|
13
|
+
* behind whichever future phase adds bulk/marketing send capability --
|
|
14
|
+
* not this one. See
|
|
15
|
+
* docs/superpowers/specs/2026-07-10-mila-email-engine-design.md §6.3/§11.
|
|
16
|
+
*/
|
|
17
|
+
export function MilaFooter() {
|
|
18
|
+
return (_jsxs("p", { className: "mila-text-muted mila-border", style: {
|
|
19
|
+
margin: 0,
|
|
20
|
+
fontFamily: fontStack.body,
|
|
21
|
+
fontSize: 12,
|
|
22
|
+
lineHeight: '18px',
|
|
23
|
+
color: colors.textMuted,
|
|
24
|
+
paddingTop: spacing.lg,
|
|
25
|
+
borderTop: `1px solid ${colors.border}`,
|
|
26
|
+
}, children: ["VENNYX YAZILIM DANI\u015EMANLIK A.\u015E. \u00B7", ' ', _jsx("a", { href: "mailto:support@mila.cx", className: "mila-text-muted", style: { color: colors.textMuted }, children: "support@mila.cx" })] }));
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=footer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"footer.js","sourceRoot":"","sources":["../src/footer.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAEzD;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,UAAU;IACxB,OAAO,CACL,aACE,SAAS,EAAC,6BAA6B,EACvC,KAAK,EAAE;YACL,MAAM,EAAE,CAAC;YACT,UAAU,EAAE,SAAS,CAAC,IAAI;YAC1B,QAAQ,EAAE,EAAE;YACZ,UAAU,EAAE,MAAM;YAClB,KAAK,EAAE,MAAM,CAAC,SAAS;YACvB,UAAU,EAAE,OAAO,CAAC,EAAE;YACtB,SAAS,EAAE,aAAa,MAAM,CAAC,MAAM,EAAE;SACxC,iEAEwC,GAAG,EAC5C,YAAG,IAAI,EAAC,wBAAwB,EAAC,SAAS,EAAC,iBAAiB,EAAC,KAAK,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,gCAE3F,IACF,CACL,CAAC;AACJ,CAAC"}
|
package/dist/head.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface MilaHeadProps {
|
|
2
|
+
title?: string;
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Every head-level requirement from
|
|
6
|
+
* docs/superpowers/specs/2026-07-10-mila-email-engine-design.md §5/§6.2 in
|
|
7
|
+
* one component, so no template can accidentally omit one. `charSet` is
|
|
8
|
+
* written using React's own special camelCase prop (NOT a literal
|
|
9
|
+
* `charset`-keyed spread) deliberately: confirmed empirically (rendering
|
|
10
|
+
* this component against the actual installed `react-email@6.7.0` /
|
|
11
|
+
* `react@19.2.7`) that React 19's real server renderer treats `<meta
|
|
12
|
+
* charSet>` and `<meta name="viewport">` as special "Hoistable" resources
|
|
13
|
+
* and unconditionally moves them to the very front of `<head>` -- ahead of
|
|
14
|
+
* every other child, and ahead of react-email's own `<Head>` wrapper's
|
|
15
|
+
* unconditionally-injected `Content-Type`/`x-apple-disable-message-
|
|
16
|
+
* reformatting` metas -- which is exactly the "first 1024 bytes" placement
|
|
17
|
+
* this component needs. Writing `charSet` as a plain string-keyed attribute
|
|
18
|
+
* instead renders correctly cosmetically (lowercase `charset=`) but silently
|
|
19
|
+
* opts OUT of that hoisting, since React's special-case only keys off the
|
|
20
|
+
* literal `charSet` prop name, which would push it several tags later in the
|
|
21
|
+
* document -- functionally worse despite looking more "correct" as source.
|
|
22
|
+
* The resulting HTML attribute name is emitted as `charSet` (capital S) --
|
|
23
|
+
* not a bug: HTML attribute names are case-insensitive to every conforming
|
|
24
|
+
* parser (including every mail client's), so `charSet="utf-8"` and
|
|
25
|
+
* `charset="utf-8"` are identical once parsed.
|
|
26
|
+
*/
|
|
27
|
+
export declare function MilaHead({ title }: MilaHeadProps): import("react").JSX.Element;
|
package/dist/head.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Head } from 'react-email';
|
|
3
|
+
import { colors } from './tokens.js';
|
|
4
|
+
const DARK_MODE_CSS = `
|
|
5
|
+
:root { color-scheme: light dark; supported-color-schemes: light dark; }
|
|
6
|
+
@media (prefers-color-scheme: dark) {
|
|
7
|
+
.mila-bg { background-color: ${colors.backgroundDark} !important; }
|
|
8
|
+
.mila-surface { background-color: ${colors.surfaceDark} !important; }
|
|
9
|
+
.mila-text { color: ${colors.textDark} !important; }
|
|
10
|
+
.mila-text-muted { color: ${colors.textMutedDark} !important; }
|
|
11
|
+
.mila-border { border-color: ${colors.borderDark} !important; }
|
|
12
|
+
.mila-accent-bg { background-color: ${colors.accentDark} !important; }
|
|
13
|
+
}
|
|
14
|
+
`;
|
|
15
|
+
/**
|
|
16
|
+
* The Outlook DPI-correction block MUST be a raw `<!--[if mso]>...` HTML
|
|
17
|
+
* comment directly inside <head> -- JSX has no way to emit a literal,
|
|
18
|
+
* unescaped HTML comment as a sibling node (`{/* ... *\/}` is a JS comment,
|
|
19
|
+
* stripped entirely, not rendered). The pragmatic, widely-used technique
|
|
20
|
+
* (matching how react-email's own Button/Preview components inject raw
|
|
21
|
+
* MSO-only markup internally, confirmed directly from their source) is
|
|
22
|
+
* `dangerouslySetInnerHTML` on a wrapper -- email-rendering HTML parsers
|
|
23
|
+
* (Outlook's Word engine, every webmail sanitizer) are exceptionally
|
|
24
|
+
* lenient about non-standard head content, and this exact div-wrapped
|
|
25
|
+
* pattern is what real bulletproof-email templates use in production.
|
|
26
|
+
*/
|
|
27
|
+
function MsoOfficeDocumentSettings() {
|
|
28
|
+
return (_jsx("div", { dangerouslySetInnerHTML: {
|
|
29
|
+
__html: '<!--[if mso]><noscript><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml></noscript><![endif]-->',
|
|
30
|
+
} }));
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Every head-level requirement from
|
|
34
|
+
* docs/superpowers/specs/2026-07-10-mila-email-engine-design.md §5/§6.2 in
|
|
35
|
+
* one component, so no template can accidentally omit one. `charSet` is
|
|
36
|
+
* written using React's own special camelCase prop (NOT a literal
|
|
37
|
+
* `charset`-keyed spread) deliberately: confirmed empirically (rendering
|
|
38
|
+
* this component against the actual installed `react-email@6.7.0` /
|
|
39
|
+
* `react@19.2.7`) that React 19's real server renderer treats `<meta
|
|
40
|
+
* charSet>` and `<meta name="viewport">` as special "Hoistable" resources
|
|
41
|
+
* and unconditionally moves them to the very front of `<head>` -- ahead of
|
|
42
|
+
* every other child, and ahead of react-email's own `<Head>` wrapper's
|
|
43
|
+
* unconditionally-injected `Content-Type`/`x-apple-disable-message-
|
|
44
|
+
* reformatting` metas -- which is exactly the "first 1024 bytes" placement
|
|
45
|
+
* this component needs. Writing `charSet` as a plain string-keyed attribute
|
|
46
|
+
* instead renders correctly cosmetically (lowercase `charset=`) but silently
|
|
47
|
+
* opts OUT of that hoisting, since React's special-case only keys off the
|
|
48
|
+
* literal `charSet` prop name, which would push it several tags later in the
|
|
49
|
+
* document -- functionally worse despite looking more "correct" as source.
|
|
50
|
+
* The resulting HTML attribute name is emitted as `charSet` (capital S) --
|
|
51
|
+
* not a bug: HTML attribute names are case-insensitive to every conforming
|
|
52
|
+
* parser (including every mail client's), so `charSet="utf-8"` and
|
|
53
|
+
* `charset="utf-8"` are identical once parsed.
|
|
54
|
+
*/
|
|
55
|
+
export function MilaHead({ title }) {
|
|
56
|
+
return (_jsxs(Head, { children: [_jsx("meta", { charSet: "utf-8" }), _jsx("meta", { httpEquiv: "X-UA-Compatible", content: "IE=edge" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1, user-scalable=yes" }), _jsx("meta", { name: "format-detection", content: "telephone=no, date=no, address=no, email=no, url=no" }), _jsx("meta", { name: "x-apple-disable-message-reformatting", content: "" }), _jsx("meta", { name: "color-scheme", content: "light dark" }), _jsx("meta", { name: "supported-color-schemes", content: "light dark" }), title ? _jsx("title", { children: title }) : null, _jsx(MsoOfficeDocumentSettings, {}), _jsx("style", { dangerouslySetInnerHTML: { __html: DARK_MODE_CSS } })] }));
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=head.js.map
|
package/dist/head.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"head.js","sourceRoot":"","sources":["../src/head.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACnC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAMrC,MAAM,aAAa,GAAG;;;qCAGe,MAAM,CAAC,cAAc;0CAChB,MAAM,CAAC,WAAW;4BAChC,MAAM,CAAC,QAAQ;kCACT,MAAM,CAAC,aAAa;qCACjB,MAAM,CAAC,UAAU;4CACV,MAAM,CAAC,UAAU;;GAE1D,CAAC;AAEJ;;;;;;;;;;;GAWG;AACH,SAAS,yBAAyB;IAChC,OAAO,CACL,cACE,uBAAuB,EAAE;YACvB,MAAM,EACJ,qJAAqJ;SACxJ,GACD,CACH,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,QAAQ,CAAC,EAAE,KAAK,EAAiB;IAC/C,OAAO,CACL,MAAC,IAAI,eACH,eAAM,OAAO,EAAC,OAAO,GAAG,EACxB,eAAM,SAAS,EAAC,iBAAiB,EAAC,OAAO,EAAC,SAAS,GAAG,EACtD,eAAM,IAAI,EAAC,UAAU,EAAC,OAAO,EAAC,wDAAwD,GAAG,EACzF,eAAM,IAAI,EAAC,kBAAkB,EAAC,OAAO,EAAC,qDAAqD,GAAG,EAC9F,eAAM,IAAI,EAAC,sCAAsC,EAAC,OAAO,EAAC,EAAE,GAAG,EAC/D,eAAM,IAAI,EAAC,cAAc,EAAC,OAAO,EAAC,YAAY,GAAG,EACjD,eAAM,IAAI,EAAC,yBAAyB,EAAC,OAAO,EAAC,YAAY,GAAG,EAC3D,KAAK,CAAC,CAAC,CAAC,0BAAQ,KAAK,GAAS,CAAC,CAAC,CAAC,IAAI,EACtC,KAAC,yBAAyB,KAAG,EAC7B,gBAAO,uBAAuB,EAAE,EAAE,MAAM,EAAE,aAAa,EAAE,GAAI,IACxD,CACR,CAAC;AACJ,CAAC"}
|
package/dist/html.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type * as React from 'react';
|
|
2
|
+
/** The two locales mila's email templates support. Deliberately NOT
|
|
3
|
+
* imported from `@mila/shared`'s own `AppLocale` type: `@vennyx/mila-react-template`
|
|
4
|
+
* is a PUBLISHED npm package, `@mila/shared` is `"private": true` and never
|
|
5
|
+
* published -- a public package's emitted `.d.ts` referencing an
|
|
6
|
+
* unpublished workspace type would break for any external installer. This
|
|
7
|
+
* is intentionally a small, self-contained duplicate. */
|
|
8
|
+
export type MilaLocale = 'tr' | 'en';
|
|
9
|
+
export interface MilaHtmlProps {
|
|
10
|
+
locale: MilaLocale;
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Wraps react-email's own `Html` and sets every `lang` signal research
|
|
15
|
+
* confirmed is independently load-bearing: `lang` (universal), a literal
|
|
16
|
+
* `xml:lang` (Outlook Desktop is documented to prioritize this namespaced
|
|
17
|
+
* form), and MilaBody below duplicates `lang` again on `<body>` since
|
|
18
|
+
* Yahoo/AOL are documented to strip the `<html>` element itself in some
|
|
19
|
+
* render paths, taking `lang` with it. `dir="ltr"` is hardcoded: mila has
|
|
20
|
+
* no RTL-language templates.
|
|
21
|
+
*
|
|
22
|
+
* React recognizes `xmlLang` as a special-cased camelCase prop that
|
|
23
|
+
* serializes to the literal `xml:lang` HTML attribute with no console
|
|
24
|
+
* warning — this is the correct, idiomatic way to set it.
|
|
25
|
+
*/
|
|
26
|
+
export declare function MilaHtml({ locale, children }: MilaHtmlProps): React.JSX.Element;
|
|
27
|
+
export interface MilaBodyProps {
|
|
28
|
+
locale: MilaLocale;
|
|
29
|
+
style?: React.CSSProperties;
|
|
30
|
+
children: React.ReactNode;
|
|
31
|
+
}
|
|
32
|
+
export declare function MilaBody({ locale, style, children }: MilaBodyProps): React.JSX.Element;
|
package/dist/html.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Body, Html } from 'react-email';
|
|
3
|
+
import { colors } from './tokens.js';
|
|
4
|
+
/**
|
|
5
|
+
* Wraps react-email's own `Html` and sets every `lang` signal research
|
|
6
|
+
* confirmed is independently load-bearing: `lang` (universal), a literal
|
|
7
|
+
* `xml:lang` (Outlook Desktop is documented to prioritize this namespaced
|
|
8
|
+
* form), and MilaBody below duplicates `lang` again on `<body>` since
|
|
9
|
+
* Yahoo/AOL are documented to strip the `<html>` element itself in some
|
|
10
|
+
* render paths, taking `lang` with it. `dir="ltr"` is hardcoded: mila has
|
|
11
|
+
* no RTL-language templates.
|
|
12
|
+
*
|
|
13
|
+
* React recognizes `xmlLang` as a special-cased camelCase prop that
|
|
14
|
+
* serializes to the literal `xml:lang` HTML attribute with no console
|
|
15
|
+
* warning — this is the correct, idiomatic way to set it.
|
|
16
|
+
*/
|
|
17
|
+
export function MilaHtml({ locale, children }) {
|
|
18
|
+
return (_jsx(Html, { lang: locale, dir: "ltr", ...{ xmlLang: locale }, children: children }));
|
|
19
|
+
}
|
|
20
|
+
export function MilaBody({ locale, style, children }) {
|
|
21
|
+
return (_jsx(Body, { lang: locale, className: "mila-bg mila-text", style: {
|
|
22
|
+
margin: 0,
|
|
23
|
+
padding: 0,
|
|
24
|
+
backgroundColor: colors.background,
|
|
25
|
+
color: colors.text,
|
|
26
|
+
...style,
|
|
27
|
+
}, children: children }));
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=html.js.map
|
package/dist/html.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html.js","sourceRoot":"","sources":["../src/html.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAerC;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,QAAQ,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAiB;IAC1D,OAAO,CACL,KAAC,IAAI,IAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAC,KAAK,KAAM,EAAE,OAAO,EAAE,MAAM,EAAU,YAC3D,QAAQ,GACJ,CACR,CAAC;AACJ,CAAC;AAQD,MAAM,UAAU,QAAQ,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAiB;IACjE,OAAO,CACL,KAAC,IAAI,IACH,IAAI,EAAE,MAAM,EACZ,SAAS,EAAC,mBAAmB,EAC7B,KAAK,EAAE;YACL,MAAM,EAAE,CAAC;YACT,OAAO,EAAE,CAAC;YACV,eAAe,EAAE,MAAM,CAAC,UAAU;YAClC,KAAK,EAAE,MAAM,CAAC,IAAI;YAClB,GAAG,KAAK;SACT,YAEA,QAAQ,GACJ,CACR,CAAC;AACJ,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from './tokens.js';
|
|
2
|
+
export * from './html.js';
|
|
3
|
+
export * from './head.js';
|
|
4
|
+
export * from './logo.js';
|
|
5
|
+
export * from './layout.js';
|
|
6
|
+
export * from './button.js';
|
|
7
|
+
export * from './divider.js';
|
|
8
|
+
export * from './otp-code.js';
|
|
9
|
+
export * from './footer.js';
|
|
10
|
+
export * from './render.js';
|
|
11
|
+
export * from './lint.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from './tokens.js';
|
|
2
|
+
export * from './html.js';
|
|
3
|
+
export * from './head.js';
|
|
4
|
+
export * from './logo.js';
|
|
5
|
+
export * from './layout.js';
|
|
6
|
+
export * from './button.js';
|
|
7
|
+
export * from './divider.js';
|
|
8
|
+
export * from './otp-code.js';
|
|
9
|
+
export * from './footer.js';
|
|
10
|
+
export * from './render.js';
|
|
11
|
+
export * from './lint.js';
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC"}
|
package/dist/layout.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type * as React from 'react';
|
|
2
|
+
import { type MilaLocale } from './html.js';
|
|
3
|
+
export interface MilaEmailLayoutProps {
|
|
4
|
+
locale: MilaLocale;
|
|
5
|
+
/** The inbox-list preview snippet (subject-adjacent text). Kept under
|
|
6
|
+
* ~150 chars by convention (client-specific budgets vary and are
|
|
7
|
+
* unreliable, see the research doc's §8) -- the Preview component pads
|
|
8
|
+
* the rest with invisible characters automatically. */
|
|
9
|
+
previewText: string;
|
|
10
|
+
/** Optional <title> -- most clients ignore it, included for completeness. */
|
|
11
|
+
title?: string;
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The hybrid ("spongy") responsive skeleton every template renders exactly
|
|
16
|
+
* once. Outlook Desktop (Word engine) ignores CSS max-width entirely, so it
|
|
17
|
+
* gets a real, separate, fixed-600px HTML table via `<!--[if mso]>`; every
|
|
18
|
+
* other client renders a fluid `width="100%" style="max-width:600px"`
|
|
19
|
+
* table that shrinks on small screens. See
|
|
20
|
+
* docs/superpowers/specs/2026-07-10-mila-email-engine-design.md §5 and the
|
|
21
|
+
* research doc's §4 for the exact pattern this mirrors.
|
|
22
|
+
*/
|
|
23
|
+
export declare function MilaEmailLayout({ locale, previewText, title, children }: MilaEmailLayoutProps): React.JSX.Element;
|
package/dist/layout.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Container, Preview } from 'react-email';
|
|
3
|
+
import { MilaBody, MilaHtml } from './html.js';
|
|
4
|
+
import { MilaHead } from './head.js';
|
|
5
|
+
import { MilaLogo } from './logo.js';
|
|
6
|
+
import { MilaFooter } from './footer.js';
|
|
7
|
+
import { containerWidth, spacing } from './tokens.js';
|
|
8
|
+
/**
|
|
9
|
+
* The hybrid ("spongy") responsive skeleton every template renders exactly
|
|
10
|
+
* once. Outlook Desktop (Word engine) ignores CSS max-width entirely, so it
|
|
11
|
+
* gets a real, separate, fixed-600px HTML table via `<!--[if mso]>`; every
|
|
12
|
+
* other client renders a fluid `width="100%" style="max-width:600px"`
|
|
13
|
+
* table that shrinks on small screens. See
|
|
14
|
+
* docs/superpowers/specs/2026-07-10-mila-email-engine-design.md §5 and the
|
|
15
|
+
* research doc's §4 for the exact pattern this mirrors.
|
|
16
|
+
*/
|
|
17
|
+
export function MilaEmailLayout({ locale, previewText, title, children }) {
|
|
18
|
+
return (_jsxs(MilaHtml, { locale: locale, children: [_jsx(MilaHead, { title: title }), _jsx(Preview, { children: previewText }), _jsxs(MilaBody, { locale: locale, children: [_jsx("div", { dangerouslySetInnerHTML: {
|
|
19
|
+
__html: `<!--[if mso]>
|
|
20
|
+
<table align="center" border="0" cellspacing="0" cellpadding="0" width="${containerWidth}">
|
|
21
|
+
<tr>
|
|
22
|
+
<td align="center" valign="top" width="${containerWidth}">
|
|
23
|
+
<![endif]-->`,
|
|
24
|
+
} }), _jsx(Container, { style: { maxWidth: containerWidth, width: '100%', padding: `${spacing.md}px ${spacing.sm}px` }, children: _jsx("table", { role: "presentation", width: "100%", border: 0, cellPadding: "0", cellSpacing: "0", children: _jsxs("tbody", { children: [_jsx("tr", { children: _jsx("td", { style: { paddingBottom: spacing.md }, children: _jsx(MilaLogo, { href: "https://mila.cx", alt: "mila anasayfa / homepage" }) }) }), _jsx("tr", { children: _jsx("td", { children: children }) }), _jsx("tr", { children: _jsx("td", { children: _jsx(MilaFooter, {}) }) })] }) }) }), _jsx("div", { dangerouslySetInnerHTML: { __html: `<!--[if mso]>\n</td>\n</tr>\n</table>\n<![endif]-->` } })] })] }));
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=layout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layout.js","sourceRoot":"","sources":["../src/layout.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAmB,MAAM,WAAW,CAAC;AAChE,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AActD;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,EAAwB;IAC5F,OAAO,CACL,MAAC,QAAQ,IAAC,MAAM,EAAE,MAAM,aACtB,KAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,GAAI,EAC1B,KAAC,OAAO,cAAE,WAAW,GAAW,EAChC,MAAC,QAAQ,IAAC,MAAM,EAAE,MAAM,aACtB,cACE,uBAAuB,EAAE;4BACvB,MAAM,EAAE;0EACsD,cAAc;;yCAE/C,cAAc;aAC1C;yBACF,GACD,EACF,KAAC,SAAS,IAAC,KAAK,EAAE,EAAE,QAAQ,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC,EAAE,IAAI,EAAE,YACvG,gBAAO,IAAI,EAAC,cAAc,EAAC,KAAK,EAAC,MAAM,EAAC,MAAM,EAAE,CAAC,EAAE,WAAW,EAAC,GAAG,EAAC,WAAW,EAAC,GAAG,YAChF,4BACE,uBACE,aAAI,KAAK,EAAE,EAAE,aAAa,EAAE,OAAO,CAAC,EAAE,EAAE,YACtC,KAAC,QAAQ,IAAC,IAAI,EAAC,iBAAiB,EAAC,GAAG,EAAC,0BAA0B,GAAG,GAC/D,GACF,EACL,uBACE,uBAAK,QAAQ,GAAM,GAChB,EACL,uBACE,uBACE,KAAC,UAAU,KAAG,GACX,GACF,IACC,GACF,GACE,EACZ,cAAK,uBAAuB,EAAE,EAAE,MAAM,EAAE,qDAAqD,EAAE,GAAI,IAC1F,IACF,CACZ,CAAC;AACJ,CAAC"}
|
package/dist/lint.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small, self-contained static-assertion suite against a template's
|
|
3
|
+
* final rendered HTML string -- NOT a full caniemail-backed compatibility
|
|
4
|
+
* linter (react-email's own approach, per research pass 3): proportionate
|
|
5
|
+
* to mila's six-template internal library rather than a general-purpose
|
|
6
|
+
* multi-client tool. Checks every hard rule from
|
|
7
|
+
* docs/superpowers/specs/2026-07-10-mila-email-engine-design.md §5/§10.
|
|
8
|
+
* Returns an array of human-readable violation strings; an empty array
|
|
9
|
+
* means the template is compliant.
|
|
10
|
+
*/
|
|
11
|
+
export declare function assertMilaEmailCompliant(html: string): string[];
|
package/dist/lint.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const SIZE_CEILING_BYTES = 40_000;
|
|
2
|
+
/**
|
|
3
|
+
* A small, self-contained static-assertion suite against a template's
|
|
4
|
+
* final rendered HTML string -- NOT a full caniemail-backed compatibility
|
|
5
|
+
* linter (react-email's own approach, per research pass 3): proportionate
|
|
6
|
+
* to mila's six-template internal library rather than a general-purpose
|
|
7
|
+
* multi-client tool. Checks every hard rule from
|
|
8
|
+
* docs/superpowers/specs/2026-07-10-mila-email-engine-design.md §5/§10.
|
|
9
|
+
* Returns an array of human-readable violation strings; an empty array
|
|
10
|
+
* means the template is compliant.
|
|
11
|
+
*/
|
|
12
|
+
export function assertMilaEmailCompliant(html) {
|
|
13
|
+
const violations = [];
|
|
14
|
+
const headMatch = html.match(/<head[^>]*>([\s\S]*?)<\/head>/i);
|
|
15
|
+
const headContent = headMatch?.[1] ?? '';
|
|
16
|
+
const first200OfHead = headContent.slice(0, 200);
|
|
17
|
+
if (!/charset="utf-8"/i.test(first200OfHead)) {
|
|
18
|
+
violations.push('Missing <meta charset="utf-8"> as an early child of <head> (must be within the first ~1024 bytes).');
|
|
19
|
+
}
|
|
20
|
+
if (!/name="color-scheme"/i.test(headContent)) {
|
|
21
|
+
violations.push('Missing <meta name="color-scheme" content="light dark">.');
|
|
22
|
+
}
|
|
23
|
+
if (!/name="supported-color-schemes"/i.test(headContent)) {
|
|
24
|
+
violations.push('Missing <meta name="supported-color-schemes" content="light dark">.');
|
|
25
|
+
}
|
|
26
|
+
const htmlTagMatch = html.match(/<html\b[^>]*>/i);
|
|
27
|
+
const htmlOpenTag = htmlTagMatch?.[0] ?? '';
|
|
28
|
+
if (!/(?:^|\s)lang="[a-z]{2}"/i.test(htmlOpenTag)) {
|
|
29
|
+
violations.push('Missing lang="<locale>" on the <html> element.');
|
|
30
|
+
}
|
|
31
|
+
if (!/xml:lang="[a-z]{2}"/i.test(htmlOpenTag)) {
|
|
32
|
+
violations.push('Missing xml:lang="<locale>" on the <html> element.');
|
|
33
|
+
}
|
|
34
|
+
const imgTags = html.match(/<img\b[^>]*>/gi) ?? [];
|
|
35
|
+
for (const img of imgTags) {
|
|
36
|
+
if (!/\balt="/.test(img)) {
|
|
37
|
+
violations.push(`<img> missing an alt attribute: ${img.slice(0, 80)}`);
|
|
38
|
+
}
|
|
39
|
+
if (/\bsrc="data:/.test(img)) {
|
|
40
|
+
violations.push(`<img> uses a base64 data: URI (never allowed -- always externally-hosted): ${img.slice(0, 80)}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const presentationTables = html.match(/<table\b[^>]*role="presentation"[^>]*>/gi) ?? [];
|
|
44
|
+
for (const table of presentationTables) {
|
|
45
|
+
const hasBorder = /\bborder="0"/i.test(table);
|
|
46
|
+
const hasCellPadding = /\bcellpadding="0"/i.test(table);
|
|
47
|
+
const hasCellSpacing = /\bcellspacing="0"/i.test(table);
|
|
48
|
+
if (!hasBorder || !hasCellPadding || !hasCellSpacing) {
|
|
49
|
+
violations.push(`<table role="presentation"> missing a legacy border="0"/cellpadding="0"/cellspacing="0" attribute: ${table.slice(0, 100)}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const byteSize = Buffer.byteLength(html, 'utf8');
|
|
53
|
+
if (byteSize > SIZE_CEILING_BYTES) {
|
|
54
|
+
violations.push(`Rendered HTML is ${byteSize} bytes, exceeding the 40KB (${SIZE_CEILING_BYTES} byte) size ceiling.`);
|
|
55
|
+
}
|
|
56
|
+
return violations;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=lint.js.map
|
package/dist/lint.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lint.js","sourceRoot":"","sources":["../src/lint.ts"],"names":[],"mappings":"AAAA,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAElC;;;;;;;;;GASG;AACH,MAAM,UAAU,wBAAwB,CAAC,IAAY;IACnD,MAAM,UAAU,GAAa,EAAE,CAAC;IAEhC,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,gCAAgC,CAAC,CAAC;IAC/D,MAAM,WAAW,GAAG,SAAS,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACzC,MAAM,cAAc,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACjD,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,cAAc,CAAC,EAAE,CAAC;QAC7C,UAAU,CAAC,IAAI,CAAC,oGAAoG,CAAC,CAAC;IACxH,CAAC;IAED,IAAI,CAAC,sBAAsB,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;QAC9C,UAAU,CAAC,IAAI,CAAC,0DAA0D,CAAC,CAAC;IAC9E,CAAC;IACD,IAAI,CAAC,iCAAiC,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;QACzD,UAAU,CAAC,IAAI,CAAC,qEAAqE,CAAC,CAAC;IACzF,CAAC;IAED,MAAM,YAAY,GAAG,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC;IAClD,MAAM,WAAW,GAAG,YAAY,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC5C,IAAI,CAAC,0BAA0B,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;QAClD,UAAU,CAAC,IAAI,CAAC,gDAAgD,CAAC,CAAC;IACpE,CAAC;IACD,IAAI,CAAC,sBAAsB,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;QAC9C,UAAU,CAAC,IAAI,CAAC,oDAAoD,CAAC,CAAC;IACxE,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,IAAI,EAAE,CAAC;IACnD,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;QAC1B,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YACzB,UAAU,CAAC,IAAI,CAAC,mCAAmC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;QACzE,CAAC;QACD,IAAI,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YAC7B,UAAU,CAAC,IAAI,CAAC,8EAA8E,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;QACpH,CAAC;IACH,CAAC;IAED,MAAM,kBAAkB,GAAG,IAAI,CAAC,KAAK,CAAC,0CAA0C,CAAC,IAAI,EAAE,CAAC;IACxF,KAAK,MAAM,KAAK,IAAI,kBAAkB,EAAE,CAAC;QACvC,MAAM,SAAS,GAAG,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC9C,MAAM,cAAc,GAAG,oBAAoB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACxD,MAAM,cAAc,GAAG,oBAAoB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACxD,IAAI,CAAC,SAAS,IAAI,CAAC,cAAc,IAAI,CAAC,cAAc,EAAE,CAAC;YACrD,UAAU,CAAC,IAAI,CAAC,sGAAsG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;QAC/I,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACjD,IAAI,QAAQ,GAAG,kBAAkB,EAAE,CAAC;QAClC,UAAU,CAAC,IAAI,CAAC,oBAAoB,QAAQ,+BAA+B,kBAAkB,sBAAsB,CAAC,CAAC;IACvH,CAAC;IAED,OAAO,UAAU,CAAC;AACpB,CAAC"}
|
package/dist/logo.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface MilaLogoProps {
|
|
2
|
+
/** Wraps the mark in a link when provided (e.g. the site homepage). */
|
|
3
|
+
href?: string;
|
|
4
|
+
/** Descriptive alt text. Defaults to "mila" -- override with a fuller
|
|
5
|
+
* destination description (e.g. "mila homepage") when `href` is set,
|
|
6
|
+
* since a linked image needs to describe its destination/action for
|
|
7
|
+
* screen-reader users, not just be decorative (never alt=""). */
|
|
8
|
+
alt?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* mila's email header mark: the dove icon (a single retina PNG asset --
|
|
12
|
+
* see scripts/generate-email-logo.mjs; inline SVG is unreliable in email,
|
|
13
|
+
* see this file's own git history/plan) plus the real "mila" wordmark as
|
|
14
|
+
* live HTML text (Inter 600, -0.03em letter-spacing, matching the site's
|
|
15
|
+
* own MilaLogo component's wordmark treatment) -- never baked into the
|
|
16
|
+
* image, both for accessibility (screen readers, research §11) and so it
|
|
17
|
+
* stays crisp/never blurry regardless of zoom level.
|
|
18
|
+
*
|
|
19
|
+
* Sits on an explicit near-white background plate (`colors.background`,
|
|
20
|
+
* #FEFEFE) rather than shipping a second dark-mode-colored logo variant --
|
|
21
|
+
* the mark's own violet gradient has enough contrast against both a light
|
|
22
|
+
* and a dark surface on its own; guaranteeing the PLATE's color (rather
|
|
23
|
+
* than the mark's colors) is the more robust of the two dark-mode-logo
|
|
24
|
+
* techniques research identified, and avoids inventing new brand artwork
|
|
25
|
+
* outside this plan's engineering scope.
|
|
26
|
+
*/
|
|
27
|
+
export declare function MilaLogo({ href, alt }: MilaLogoProps): import("react").JSX.Element;
|
package/dist/logo.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Img, Link, Section } from 'react-email';
|
|
3
|
+
import { colors, fontStack } from './tokens.js';
|
|
4
|
+
const LOGO_URL = 'https://mila.cx/brand/email/mila-dove-email@2x.png';
|
|
5
|
+
/**
|
|
6
|
+
* mila's email header mark: the dove icon (a single retina PNG asset --
|
|
7
|
+
* see scripts/generate-email-logo.mjs; inline SVG is unreliable in email,
|
|
8
|
+
* see this file's own git history/plan) plus the real "mila" wordmark as
|
|
9
|
+
* live HTML text (Inter 600, -0.03em letter-spacing, matching the site's
|
|
10
|
+
* own MilaLogo component's wordmark treatment) -- never baked into the
|
|
11
|
+
* image, both for accessibility (screen readers, research §11) and so it
|
|
12
|
+
* stays crisp/never blurry regardless of zoom level.
|
|
13
|
+
*
|
|
14
|
+
* Sits on an explicit near-white background plate (`colors.background`,
|
|
15
|
+
* #FEFEFE) rather than shipping a second dark-mode-colored logo variant --
|
|
16
|
+
* the mark's own violet gradient has enough contrast against both a light
|
|
17
|
+
* and a dark surface on its own; guaranteeing the PLATE's color (rather
|
|
18
|
+
* than the mark's colors) is the more robust of the two dark-mode-logo
|
|
19
|
+
* techniques research identified, and avoids inventing new brand artwork
|
|
20
|
+
* outside this plan's engineering scope.
|
|
21
|
+
*/
|
|
22
|
+
export function MilaLogo({ href, alt = 'mila' }) {
|
|
23
|
+
const mark = (_jsx(Section, { style: { backgroundColor: colors.background }, children: _jsx("table", { role: "presentation", border: 0, cellPadding: "0", cellSpacing: "0", align: "left", children: _jsx("tbody", { children: _jsxs("tr", { children: [_jsx("td", { style: { paddingRight: 8, verticalAlign: 'middle' }, children: _jsx(Img, { src: LOGO_URL, width: 32, height: 32, alt: alt, style: { display: 'block', border: '0', outline: 'none', textDecoration: 'none' } }) }), _jsx("td", { style: { verticalAlign: 'middle' }, children: _jsx("span", { style: {
|
|
24
|
+
fontFamily: fontStack.heading,
|
|
25
|
+
fontWeight: 600,
|
|
26
|
+
fontSize: 20,
|
|
27
|
+
letterSpacing: '-0.03em',
|
|
28
|
+
color: colors.text,
|
|
29
|
+
}, children: "mila" }) })] }) }) }) }));
|
|
30
|
+
if (!href)
|
|
31
|
+
return mark;
|
|
32
|
+
return (_jsx(Link, { href: href, style: { textDecoration: 'none' }, children: mark }));
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=logo.js.map
|
package/dist/logo.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logo.js","sourceRoot":"","sources":["../src/logo.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAYhD,MAAM,QAAQ,GAAG,oDAAoD,CAAC;AAEtE;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,QAAQ,CAAC,EAAE,IAAI,EAAE,GAAG,GAAG,MAAM,EAAiB;IAC5D,MAAM,IAAI,GAAG,CAKX,KAAC,OAAO,IAAC,KAAK,EAAE,EAAE,eAAe,EAAE,MAAM,CAAC,UAAU,EAAE,YACpD,gBAAO,IAAI,EAAC,cAAc,EAAC,MAAM,EAAE,CAAC,EAAE,WAAW,EAAC,GAAG,EAAC,WAAW,EAAC,GAAG,EAAC,KAAK,EAAC,MAAM,YAChF,0BACE,yBACE,aAAI,KAAK,EAAE,EAAE,YAAY,EAAE,CAAC,EAAE,aAAa,EAAE,QAAQ,EAAE,YACrD,KAAC,GAAG,IAAC,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,GAAI,GACvI,EACL,aAAI,KAAK,EAAE,EAAE,aAAa,EAAE,QAAQ,EAAE,YACpC,eACE,KAAK,EAAE;oCACL,UAAU,EAAE,SAAS,CAAC,OAAO;oCAC7B,UAAU,EAAE,GAAG;oCACf,QAAQ,EAAE,EAAE;oCACZ,aAAa,EAAE,SAAS;oCACxB,KAAK,EAAE,MAAM,CAAC,IAAI;iCACnB,qBAGI,GACJ,IACF,GACC,GACF,GACA,CACX,CAAC;IAEF,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IAEvB,OAAO,CACL,KAAC,IAAI,IAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,cAAc,EAAE,MAAM,EAAE,YAChD,IAAI,GACA,CACR,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface MilaOtpCodeProps {
|
|
2
|
+
code: string;
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* A monospace, letter-spaced code/OTP display block for invitation and
|
|
6
|
+
* verification flows. Uses the same web-safe monospace stack fallback
|
|
7
|
+
* discipline as every other text element (Outlook Desktop falls back to
|
|
8
|
+
* Times New Roman -- monospace, unlike sans-serif body text, doesn't have
|
|
9
|
+
* an equally universal fallback story, but `ui-monospace` (Apple/Chrome's
|
|
10
|
+
* own system-mono keyword) -> SFMono-Regular -> Menlo -> Consolas ->
|
|
11
|
+
* generic monospace covers every realistic client).
|
|
12
|
+
*/
|
|
13
|
+
export declare function MilaOtpCode({ code }: MilaOtpCodeProps): import("react").JSX.Element;
|
package/dist/otp-code.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Section } from 'react-email';
|
|
3
|
+
import { colors, fontStack } from './tokens.js';
|
|
4
|
+
/**
|
|
5
|
+
* A monospace, letter-spaced code/OTP display block for invitation and
|
|
6
|
+
* verification flows. Uses the same web-safe monospace stack fallback
|
|
7
|
+
* discipline as every other text element (Outlook Desktop falls back to
|
|
8
|
+
* Times New Roman -- monospace, unlike sans-serif body text, doesn't have
|
|
9
|
+
* an equally universal fallback story, but `ui-monospace` (Apple/Chrome's
|
|
10
|
+
* own system-mono keyword) -> SFMono-Regular -> Menlo -> Consolas ->
|
|
11
|
+
* generic monospace covers every realistic client).
|
|
12
|
+
*/
|
|
13
|
+
export function MilaOtpCode({ code }) {
|
|
14
|
+
return (_jsx(Section, { style: {
|
|
15
|
+
backgroundColor: colors.surface,
|
|
16
|
+
borderRadius: 8,
|
|
17
|
+
padding: '20px 24px',
|
|
18
|
+
textAlign: 'center',
|
|
19
|
+
}, children: _jsx("span", { className: "mila-text", style: {
|
|
20
|
+
fontFamily: fontStack.mono,
|
|
21
|
+
fontSize: 28,
|
|
22
|
+
fontWeight: 600,
|
|
23
|
+
letterSpacing: '0.3em',
|
|
24
|
+
color: colors.text,
|
|
25
|
+
}, children: code }) }));
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=otp-code.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"otp-code.js","sourceRoot":"","sources":["../src/otp-code.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAMhD;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CAAC,EAAE,IAAI,EAAoB;IACpD,OAAO,CACL,KAAC,OAAO,IACN,KAAK,EAAE;YACL,eAAe,EAAE,MAAM,CAAC,OAAO;YAC/B,YAAY,EAAE,CAAC;YACf,OAAO,EAAE,WAAW;YACpB,SAAS,EAAE,QAAQ;SACpB,YAED,eACE,SAAS,EAAC,WAAW,EACrB,KAAK,EAAE;gBACL,UAAU,EAAE,SAAS,CAAC,IAAI;gBAC1B,QAAQ,EAAE,EAAE;gBACZ,UAAU,EAAE,GAAG;gBACf,aAAa,EAAE,OAAO;gBACtB,KAAK,EAAE,MAAM,CAAC,IAAI;aACnB,YAEA,IAAI,GACA,GACC,CACX,CAAC;AACJ,CAAC"}
|
package/dist/render.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type * as React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Thin wrapper over react-email's own `render()`/`toPlainText()` -- both
|
|
4
|
+
* verified directly from react-email@6.7.0's real published types/source
|
|
5
|
+
* (packages/render's `render(node, options?): Promise<string>` and
|
|
6
|
+
* `toPlainText(html, options?): string`, the latter itself wrapping
|
|
7
|
+
* `html-to-text` internally). No separate `html-to-text` dependency is
|
|
8
|
+
* declared in this package's own package.json: react-email already pulls
|
|
9
|
+
* it in transitively and re-exports the wrapper function, so depending on
|
|
10
|
+
* it again directly would just be duplicate version-pinning for no
|
|
11
|
+
* benefit.
|
|
12
|
+
*
|
|
13
|
+
* `hideLinkHrefIfSameAsText` and `img: {format: 'skip'}` match
|
|
14
|
+
* docs/superpowers/specs/2026-07-10-mila-email-engine-design.md §5 rule 14
|
|
15
|
+
* / §6.5 exactly. `wordwrap: 78` is RFC 5322's own recommended soft-wrap
|
|
16
|
+
* width.
|
|
17
|
+
*/
|
|
18
|
+
export declare function renderMilaEmail(element: React.ReactElement): Promise<{
|
|
19
|
+
html: string;
|
|
20
|
+
text: string;
|
|
21
|
+
}>;
|
package/dist/render.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { render, toPlainText } from 'react-email';
|
|
2
|
+
/**
|
|
3
|
+
* Thin wrapper over react-email's own `render()`/`toPlainText()` -- both
|
|
4
|
+
* verified directly from react-email@6.7.0's real published types/source
|
|
5
|
+
* (packages/render's `render(node, options?): Promise<string>` and
|
|
6
|
+
* `toPlainText(html, options?): string`, the latter itself wrapping
|
|
7
|
+
* `html-to-text` internally). No separate `html-to-text` dependency is
|
|
8
|
+
* declared in this package's own package.json: react-email already pulls
|
|
9
|
+
* it in transitively and re-exports the wrapper function, so depending on
|
|
10
|
+
* it again directly would just be duplicate version-pinning for no
|
|
11
|
+
* benefit.
|
|
12
|
+
*
|
|
13
|
+
* `hideLinkHrefIfSameAsText` and `img: {format: 'skip'}` match
|
|
14
|
+
* docs/superpowers/specs/2026-07-10-mila-email-engine-design.md §5 rule 14
|
|
15
|
+
* / §6.5 exactly. `wordwrap: 78` is RFC 5322's own recommended soft-wrap
|
|
16
|
+
* width.
|
|
17
|
+
*/
|
|
18
|
+
export async function renderMilaEmail(element) {
|
|
19
|
+
const html = await render(element);
|
|
20
|
+
const text = toPlainText(html, {
|
|
21
|
+
wordwrap: 78,
|
|
22
|
+
selectors: [
|
|
23
|
+
{ selector: 'a', options: { hideLinkHrefIfSameAsText: true } },
|
|
24
|
+
{ selector: 'img', format: 'skip' },
|
|
25
|
+
],
|
|
26
|
+
});
|
|
27
|
+
return { html, text };
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=render.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAElD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,OAA2B;IAC/D,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,CAAC;IACnC,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,EAAE;QAC7B,QAAQ,EAAE,EAAE;QACZ,SAAS,EAAE;YACT,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,wBAAwB,EAAE,IAAI,EAAE,EAAE;YAC9D,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE;SACpC;KACF,CAAC,CAAC;IACH,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;AACxB,CAAC"}
|
package/dist/tokens.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Design tokens for mila's email templates. Colors are pulled directly from
|
|
3
|
+
* apps/site's/apps/panel's own Chakra `mila` color ramp
|
|
4
|
+
* (apps/site/src/lib/theme.ts) so email stays visually consistent with the
|
|
5
|
+
* rest of the product -- NOT re-derived or approximated.
|
|
6
|
+
*
|
|
7
|
+
* Near-pure (not pure) black/white values are deliberate: Apple Mail is
|
|
8
|
+
* reported (practitioner-observed, not Apple-documented) to auto-invert
|
|
9
|
+
* literal #000000/#FFFFFF more aggressively than off-black/off-white, and
|
|
10
|
+
* off-pure values also improve WCAG contrast headroom regardless of that
|
|
11
|
+
* behavior. See docs/superpowers/specs/2026-07-10-mila-email-engine-design.md
|
|
12
|
+
* §5 rule 9 and §8.
|
|
13
|
+
*
|
|
14
|
+
* The dark-mode neutrals (backgroundDark/surfaceDark/borderDark) are NOT
|
|
15
|
+
* pulled from Chakra's own default dark theme -- Chakra's runtime tokens
|
|
16
|
+
* don't exist at email-render time (email HTML can't load a CSS-in-JS
|
|
17
|
+
* runtime), so these are a deliberate, self-contained, email-only choice: a
|
|
18
|
+
* standard near-black dark surface (#121212, a widely-used dark-UI base),
|
|
19
|
+
* paired with mila.400 (the ramp's own next-lighter step from mila.600) as
|
|
20
|
+
* the dark-mode accent, since a saturated mila.600 is too dark to read
|
|
21
|
+
* clearly on a near-black background.
|
|
22
|
+
*/
|
|
23
|
+
export declare const colors: {
|
|
24
|
+
readonly accent: "#7c3aed";
|
|
25
|
+
readonly accentDark: "#a78bfa";
|
|
26
|
+
readonly text: "#0E0E0E";
|
|
27
|
+
readonly textMuted: "#5b5b66";
|
|
28
|
+
readonly textDark: "#FEFEFE";
|
|
29
|
+
readonly textMutedDark: "#a3a3ad";
|
|
30
|
+
readonly background: "#FEFEFE";
|
|
31
|
+
readonly backgroundDark: "#121212";
|
|
32
|
+
readonly surface: "#f5f3ff";
|
|
33
|
+
readonly surfaceDark: "#1f1a2e";
|
|
34
|
+
readonly border: "#ddd6fe";
|
|
35
|
+
readonly borderDark: "#3a3350";
|
|
36
|
+
readonly buttonText: "#FEFEFE";
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Full web-safe fallback stack behind mila's Inter brand font. Order
|
|
40
|
+
* matters (docs/superpowers/specs/2026-07-10-mila-email-engine-design.md
|
|
41
|
+
* §5 rule 6): Inter first (only clients that load @font-face pick it up) ->
|
|
42
|
+
* -apple-system/BlinkMacSystemFont (San Francisco on Apple Mail/iOS) ->
|
|
43
|
+
* Segoe UI (effective Outlook Desktop/Windows rendering regardless of CSS)
|
|
44
|
+
* -> Roboto (Gmail's effective default) -> Helvetica/Arial/sans-serif
|
|
45
|
+
* (universal safety net -- guarantees no serif fallback anywhere, since
|
|
46
|
+
* Outlook Desktop falls back all the way to Times New Roman if the whole
|
|
47
|
+
* stack were ever ignored).
|
|
48
|
+
*/
|
|
49
|
+
export declare const fontStack: {
|
|
50
|
+
readonly heading: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif";
|
|
51
|
+
readonly body: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif";
|
|
52
|
+
readonly mono: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
|
|
53
|
+
};
|
|
54
|
+
export declare const spacing: {
|
|
55
|
+
readonly xs: 8;
|
|
56
|
+
readonly sm: 16;
|
|
57
|
+
readonly md: 24;
|
|
58
|
+
readonly lg: 32;
|
|
59
|
+
readonly xl: 48;
|
|
60
|
+
};
|
|
61
|
+
/** Matches react-email's own `Container` default (`maxWidth: '37.5em'` ==
|
|
62
|
+
* 600px at a 16px base) -- used by MilaEmailLayout's hybrid Outlook table. */
|
|
63
|
+
export declare const containerWidth = 600;
|
package/dist/tokens.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Design tokens for mila's email templates. Colors are pulled directly from
|
|
3
|
+
* apps/site's/apps/panel's own Chakra `mila` color ramp
|
|
4
|
+
* (apps/site/src/lib/theme.ts) so email stays visually consistent with the
|
|
5
|
+
* rest of the product -- NOT re-derived or approximated.
|
|
6
|
+
*
|
|
7
|
+
* Near-pure (not pure) black/white values are deliberate: Apple Mail is
|
|
8
|
+
* reported (practitioner-observed, not Apple-documented) to auto-invert
|
|
9
|
+
* literal #000000/#FFFFFF more aggressively than off-black/off-white, and
|
|
10
|
+
* off-pure values also improve WCAG contrast headroom regardless of that
|
|
11
|
+
* behavior. See docs/superpowers/specs/2026-07-10-mila-email-engine-design.md
|
|
12
|
+
* §5 rule 9 and §8.
|
|
13
|
+
*
|
|
14
|
+
* The dark-mode neutrals (backgroundDark/surfaceDark/borderDark) are NOT
|
|
15
|
+
* pulled from Chakra's own default dark theme -- Chakra's runtime tokens
|
|
16
|
+
* don't exist at email-render time (email HTML can't load a CSS-in-JS
|
|
17
|
+
* runtime), so these are a deliberate, self-contained, email-only choice: a
|
|
18
|
+
* standard near-black dark surface (#121212, a widely-used dark-UI base),
|
|
19
|
+
* paired with mila.400 (the ramp's own next-lighter step from mila.600) as
|
|
20
|
+
* the dark-mode accent, since a saturated mila.600 is too dark to read
|
|
21
|
+
* clearly on a near-black background.
|
|
22
|
+
*/
|
|
23
|
+
export const colors = {
|
|
24
|
+
accent: '#7c3aed', // mila.600
|
|
25
|
+
accentDark: '#a78bfa', // mila.400
|
|
26
|
+
text: '#0E0E0E',
|
|
27
|
+
textMuted: '#5b5b66',
|
|
28
|
+
textDark: '#FEFEFE',
|
|
29
|
+
textMutedDark: '#a3a3ad',
|
|
30
|
+
background: '#FEFEFE',
|
|
31
|
+
backgroundDark: '#121212',
|
|
32
|
+
surface: '#f5f3ff', // mila.50
|
|
33
|
+
surfaceDark: '#1f1a2e',
|
|
34
|
+
border: '#ddd6fe', // mila.200
|
|
35
|
+
borderDark: '#3a3350',
|
|
36
|
+
buttonText: '#FEFEFE',
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Full web-safe fallback stack behind mila's Inter brand font. Order
|
|
40
|
+
* matters (docs/superpowers/specs/2026-07-10-mila-email-engine-design.md
|
|
41
|
+
* §5 rule 6): Inter first (only clients that load @font-face pick it up) ->
|
|
42
|
+
* -apple-system/BlinkMacSystemFont (San Francisco on Apple Mail/iOS) ->
|
|
43
|
+
* Segoe UI (effective Outlook Desktop/Windows rendering regardless of CSS)
|
|
44
|
+
* -> Roboto (Gmail's effective default) -> Helvetica/Arial/sans-serif
|
|
45
|
+
* (universal safety net -- guarantees no serif fallback anywhere, since
|
|
46
|
+
* Outlook Desktop falls back all the way to Times New Roman if the whole
|
|
47
|
+
* stack were ever ignored).
|
|
48
|
+
*/
|
|
49
|
+
export const fontStack = {
|
|
50
|
+
heading: `'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif`,
|
|
51
|
+
body: `'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif`,
|
|
52
|
+
mono: `ui-monospace, SFMono-Regular, Menlo, Consolas, monospace`,
|
|
53
|
+
};
|
|
54
|
+
export const spacing = {
|
|
55
|
+
xs: 8,
|
|
56
|
+
sm: 16,
|
|
57
|
+
md: 24,
|
|
58
|
+
lg: 32,
|
|
59
|
+
xl: 48,
|
|
60
|
+
};
|
|
61
|
+
/** Matches react-email's own `Container` default (`maxWidth: '37.5em'` ==
|
|
62
|
+
* 600px at a 16px base) -- used by MilaEmailLayout's hybrid Outlook table. */
|
|
63
|
+
export const containerWidth = 600;
|
|
64
|
+
//# sourceMappingURL=tokens.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokens.js","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,MAAM,EAAE,SAAS,EAAE,WAAW;IAC9B,UAAU,EAAE,SAAS,EAAE,WAAW;IAClC,IAAI,EAAE,SAAS;IACf,SAAS,EAAE,SAAS;IACpB,QAAQ,EAAE,SAAS;IACnB,aAAa,EAAE,SAAS;IACxB,UAAU,EAAE,SAAS;IACrB,cAAc,EAAE,SAAS;IACzB,OAAO,EAAE,SAAS,EAAE,UAAU;IAC9B,WAAW,EAAE,SAAS;IACtB,MAAM,EAAE,SAAS,EAAE,WAAW;IAC9B,UAAU,EAAE,SAAS;IACrB,UAAU,EAAE,SAAS;CACb,CAAC;AAEX;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB,OAAO,EAAE,8FAA8F;IACvG,IAAI,EAAE,8FAA8F;IACpG,IAAI,EAAE,0DAA0D;CACxD,CAAC;AAEX,MAAM,CAAC,MAAM,OAAO,GAAG;IACrB,EAAE,EAAE,CAAC;IACL,EAAE,EAAE,EAAE;IACN,EAAE,EAAE,EAAE;IACN,EAAE,EAAE,EAAE;IACN,EAAE,EAAE,EAAE;CACE,CAAC;AAEX;8EAC8E;AAC9E,MAAM,CAAC,MAAM,cAAc,GAAG,GAAG,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vennyx/mila-react-template",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Official React email component library for mila -- bulletproof, bilingual, brand-aware transactional email templates, built on react-email.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc -b",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"generate:logo": "node scripts/generate-email-logo.mjs"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"react-email": "6.7.0"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
30
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "26.1.0",
|
|
34
|
+
"@types/react": "19.2.17",
|
|
35
|
+
"@types/react-dom": "19.2.3",
|
|
36
|
+
"react": "19.2.7",
|
|
37
|
+
"react-dom": "19.2.7",
|
|
38
|
+
"sharp": "0.34.5",
|
|
39
|
+
"typescript": "6.0.3",
|
|
40
|
+
"vitest": "4.1.9"
|
|
41
|
+
}
|
|
42
|
+
}
|