@samva/markup 0.13.1 → 0.14.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/AGENTS.md CHANGED
@@ -30,7 +30,7 @@ Authoring entries are what a template file imports:
30
30
  mode) and `BrandFooter` (company, postal address, social links and the unsubscribe row the send
31
31
  path fills). The compiler binds them to the brand the build resolves; add
32
32
  `"types": ["@samva/markup/samva-brand"]` to the project's tsconfig for `tsc`. In `theme.css`,
33
- `@import "samva:brand";` layers the brand theme; `samva:brand/<slug>` names one brand. These are
33
+ `@import "samva:brand";` layers the brand theme over the default theme (`DEFAULT_THEME`); `samva:brand/<slug>` names one brand. These are
34
34
  Samva's brand plugin (`samvaBrandPlugin` from `@samva/markup/brand`), the default; a host that
35
35
  delivers mail itself passes its own `BrandPlugin` as `brandPlugin` to `compileTemplate`,
36
36
  `renderIr` and `renderBrandFooter` to rename the brand import, the footer markers, the
@@ -65,6 +65,7 @@ entry?)` is the synchronous structural check: it runs the compiler's lowering wi
65
65
  conditional or element is a locked segment; and JSX source locations. Browser-safe: Babel parser
66
66
  only.
67
67
  - `@samva/markup/brand` — brand records and their CSS model (`parseBrandCss`, `printBrandCss`), the
68
+ theme vocabulary (`THEME_COLOR_ROLES`, `THEME_TOKENS`, `DEFAULT_THEME`, `foregroundDefaults`), the
68
69
  footer markers a send reads (`renderedFooterMarkers`, `withoutUnsubscribeRow`), and project
69
70
  assets (`isAssetPath`, `ASSET_CONTENT_TYPES`, `assetUrl`): which imported files become
70
71
  content-addressed assets. The send path imports it, so its closure holds only the css-tree parser
@@ -117,6 +118,15 @@ Read the contract before authoring. Import only subpaths declared in `package.js
117
118
  into file paths, so each must match `TEMPLATE_NAME_PATTERN`, one plain path segment.
118
119
  `defineTemplate` throws a `TypeError` where the name is declared. Code that builds a path from these
119
120
  names imports the pattern instead of restating it.
121
+ - **Naming a second set of tokens for dark mode.** A theme states dark mode once, in
122
+ `@media (prefers-color-scheme: dark) { @theme { --color-card: … } }`, with the same names as its
123
+ light `@theme`. The cascade gives every element whose inlined styles read one of those colors a
124
+ generated `sml-dark-*` class, so `bg-card` switches by itself. A `--color-card-dark` token is just
125
+ another color, and `dark:` only picks a different token. A block styled with palette colors
126
+ (`bg-indigo-600`) or raw hex ignores the theme; style with the vocabulary in `THEME_COLOR_ROLES`.
127
+ - **Reading contrast off the light render.** `checkEmailCompatibility` includes `email-contrast`,
128
+ which judges each fixture in light and dark mode (`CONTRAST_THRESHOLDS`); a color a dark block
129
+ leaves unpaired is an error there, not a visual nit.
120
130
  - **Declaring a webfont as if every client loads it.** A project font is an ordinary `@font-face`
121
131
  in the project's CSS (usually `theme.css`) whose `src` is a `.woff2` file in the project, named
122
132
  by a path relative to that stylesheet; the compiler serves it as a content-addressed asset and
package/CHANGELOG.md CHANGED
@@ -1,3 +1,45 @@
1
+ ## @samva/markup@0.14.0
2
+
3
+ ### A semantic theme vocabulary, one dark-mode mechanism and a contrast check
4
+
5
+ Every project compiles over a default theme in a shadcn-style vocabulary: `background`,
6
+ `foreground`, `card`, `primary`, `secondary`, `muted`, `accent` (each with its `-foreground`),
7
+ `border`, `--radius` (with `rounded-sm` to `rounded-xl` derived from it), `--font-sans` and
8
+ `--font-heading`. `starter.css`, a brand and the project's own `@theme` set the same names, so
9
+ `bg-card` or `text-primary-foreground` resolve in any project and a theme restyles every block that
10
+ reads them. `reference.md` and `contract.json` list the tokens, their roles and their defaults;
11
+ `@samva/markup/brand` exports `THEME_COLOR_ROLES`, `THEME_TOKENS` and `DEFAULT_THEME`.
12
+
13
+ Dark mode is one mechanism: the theme's `@media (prefers-color-scheme: dark) { @theme { … } }`
14
+ block, in project CSS and brand CSS alike. The compiler gives each element whose inlined styles
15
+ read a color that block redefines a generated `sml-dark-*` class with a dark-mode rule, so a block
16
+ writes `bg-card` once. Explicit `dark:` utilities still apply after it and resolve theme colors at
17
+ their dark values. A layer that sets a color in light mode only keeps it in dark mode, and a layer
18
+ that sets `--color-primary`, `--color-secondary` or `--color-accent` without its `-foreground` gets
19
+ black or white by contrast.
20
+
21
+ Breaking: the `-dark` suffix convention is gone. Nothing lowers a dark color to
22
+ `--color-<name>-dark` any more; move those values into the dark-mode block and drop the matching
23
+ `dark:…-dark` classes. `brandTailwindCss`, `brandThemeVariables`, `darkVariableName`,
24
+ `BRAND_COLOR_VARIABLE`, `BRAND_FOREGROUND_VARIABLE` and the `brand-css-dark-conflict` finding are
25
+ removed; `brandForegroundDefaults(brand)` is `foregroundDefaults(light, dark)` over the fills.
26
+ The brand footer and logo use `text-muted-foreground`.
27
+
28
+ The primitives read the theme. `Email` paints the page color (`--color-background` by default) on
29
+ the body and, as `bgcolor`, on the wrapper table, and pads its content 16px each side (`gutter`).
30
+ `Section` treats a pixel width as a maximum, so it fits a phone. `Columns` stacks its cells below
31
+ 600px (`stack={false}` keeps a row) and spaces them with `gap` in both layouts. `Button`, `Link`
32
+ and `Divider` default to the primary fill, the primary color and the border color through classes
33
+ a utility on them overrides; their explicit props are inline styles as before. These change the
34
+ compiled output of every template, so the email-parity golden digests change.
35
+
36
+ `checkEmailCompatibility` adds `email-contrast`: each fixture is judged in light and dark mode,
37
+ and text below 3:1 or a button below 1.5:1 against its surroundings is an error located at the
38
+ element; normal-size text below 4.5:1 and a button below 3:1 are warnings. New diagnostic codes:
39
+ `email-contrast`, `invalid-columns-stack`, `invalid-columns-gap` and `invalid-number-prop`.
40
+
41
+ The email starter is redesigned on the vocabulary from copyable blocks in `templates/blocks/`.
42
+
1
43
  ## @samva/markup@0.13.1
2
44
 
3
45
  ### The authoring reference omits channel-only upgrade recipes
@@ -33,7 +33,7 @@ interface EmailBrand {
33
33
  readonly footer?: EmailBrandFooter | undefined;
34
34
  }
35
35
  /** Every class the brand components use, compiled for any project that imports them. */
36
- declare const BRAND_COMPONENT_CLASSES: readonly ["dark:hidden!", "hidden", "dark:block!", "text-xs", "text-gray-500", "text-center", "font-semibold", "underline"];
36
+ declare const BRAND_COMPONENT_CLASSES: readonly ["dark:hidden!", "hidden", "dark:block!", "text-xs", "text-muted-foreground", "text-center", "font-semibold", "underline"];
37
37
  interface BrandLogoProps {
38
38
  /** Rendered width in pixels; the height follows the image. */
39
39
  readonly width?: number | undefined;
@@ -67,7 +67,7 @@ const BRAND_COMPONENT_CLASSES = [
67
67
  "hidden",
68
68
  "dark:block!",
69
69
  "text-xs",
70
- "text-gray-500",
70
+ "text-muted-foreground",
71
71
  "text-center",
72
72
  "font-semibold",
73
73
  "underline"
@@ -138,7 +138,7 @@ const brandComponents = (brand, plugin = samvaBrandPlugin) => {
138
138
  const links = footer.socialLinks ?? [];
139
139
  if (links.length > 0) rows.push(row(links.flatMap((link, index) => [...index === 0 ? [] : [" · "], jsx("a", {
140
140
  href: link.url,
141
- className: "text-gray-500 underline",
141
+ className: "text-muted-foreground underline",
142
142
  children: link.label
143
143
  })])));
144
144
  rows.push(row(unsubscribeLink(unsubscribeLabel, plugin), {
@@ -155,7 +155,7 @@ const brandComponents = (brand, plugin = samvaBrandPlugin) => {
155
155
  [BRAND_FOOTER_MARKER]: true,
156
156
  children: jsx("tbody", { children: jsx("tr", { children: jsx("td", {
157
157
  align: "center",
158
- className: classes("text-xs text-gray-500 text-center", className),
158
+ className: classes("text-xs text-muted-foreground text-center", className),
159
159
  style: { padding: "32px 16px" },
160
160
  children: jsx("table", {
161
161
  role: "presentation",
@@ -179,7 +179,7 @@ const brandComponents = (brand, plugin = samvaBrandPlugin) => {
179
179
  */
180
180
  const unsubscribeLink = (label, plugin) => jsx("a", {
181
181
  href: plugin.unsubscribeUrlPlaceholder,
182
- className: "text-gray-500 underline",
182
+ className: "text-muted-foreground underline",
183
183
  [UNSUBSCRIBE_LINK]: true,
184
184
  children: label
185
185
  });
package/dist/brand.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { t as BrandPlugin } from "./brand-plugin-CHhcei-j.js";
2
- import { _ as EmailBrandSocialLink, a as assetUrl, b as renderedFooterMarkers, c as isExternalAsset, d as BrandFooterProps, f as BrandLogoProps, g as EmailBrandFooter, h as EmailBrandAssets, i as assetEntry, l as BRAND_COMPONENT_CLASSES, m as EmailBrandAddress, n as AssetEntry, o as checkAssetBase, p as EmailBrand, r as AssetManifest, s as isAssetPath, t as ASSET_CONTENT_TYPES, u as BrandComponents, v as RenderedFooterMarkers, x as withoutUnsubscribeRow, y as brandComponents } from "./assets-DCULM6QD.js";
2
+ import { _ as EmailBrandSocialLink, a as assetUrl, b as renderedFooterMarkers, c as isExternalAsset, d as BrandFooterProps, f as BrandLogoProps, g as EmailBrandFooter, h as EmailBrandAssets, i as assetEntry, l as BRAND_COMPONENT_CLASSES, m as EmailBrandAddress, n as AssetEntry, o as checkAssetBase, p as EmailBrand, r as AssetManifest, s as isAssetPath, t as ASSET_CONTENT_TYPES, u as BrandComponents, v as RenderedFooterMarkers, x as withoutUnsubscribeRow, y as brandComponents } from "./assets-tISHaQNF.js";
3
3
  import { t as Diagnostic } from "./diagnostic-model-DxthUZBT.js";
4
4
  import { Result } from "better-result";
5
5
  //#region src/email/samva-brand-plugin.d.ts
@@ -29,8 +29,6 @@ interface BrandCss {
29
29
  readonly dark: Readonly<Record<string, string>>;
30
30
  readonly fontFaces: readonly BrandFontFace[];
31
31
  }
32
- /** The theme variable a dark-mode color lowers to: `--color-brand` → `--color-brand-dark`. */
33
- declare const darkVariableName: (property: string) => string;
34
32
  /** Parse brand theme CSS against the closed grammar above. */
35
33
  declare const parseBrandCss: (css: string) => Result<BrandCss, ReadonlyArray<Diagnostic>>;
36
34
  /**
@@ -39,9 +37,43 @@ declare const parseBrandCss: (css: string) => Result<BrandCss, ReadonlyArray<Dia
39
37
  * output always parses back to the same value.
40
38
  */
41
39
  declare const printBrandCss: (brand: BrandCss) => string;
42
- /** The brand color, and the text color that sits on it. */
43
- declare const BRAND_COLOR_VARIABLE = "--color-brand";
44
- declare const BRAND_FOREGROUND_VARIABLE = "--color-brand-foreground";
40
+ /**
41
+ * The semantic color tokens, as Tailwind color names (`bg-card`,
42
+ * `text-primary-foreground`, `border-border`), each with the role a block uses
43
+ * it for. A block reads only these, so swapping the theme reskins it.
44
+ */
45
+ declare const THEME_COLOR_ROLES: {
46
+ readonly background: "The page, behind everything.";
47
+ readonly foreground: "Text on the page.";
48
+ readonly card: "A surface that groups the message's content.";
49
+ readonly "card-foreground": "Text on a card.";
50
+ readonly primary: "The main action's fill (a button), and links.";
51
+ readonly "primary-foreground": "Text on a primary fill.";
52
+ readonly secondary: "A quieter action's fill.";
53
+ readonly "secondary-foreground": "Text on a secondary fill.";
54
+ readonly muted: "A subdued surface: a code block, a table header.";
55
+ readonly "muted-foreground": "Secondary text on the page, a card or a muted surface: labels, captions, the footer.";
56
+ readonly accent: "A highlighted surface: a callout, a selected row.";
57
+ readonly "accent-foreground": "Text on an accent surface.";
58
+ readonly border: "Rules, dividers and outlines.";
59
+ };
60
+ declare const THEME_COLORS: readonly (keyof typeof THEME_COLOR_ROLES)[];
61
+ type ThemeColor = (typeof THEME_COLORS)[number];
62
+ /**
63
+ * The non-color tokens: the corner radius (`rounded`; `rounded-sm` to `rounded-xl` follow it) and
64
+ * the body and heading families (`font-sans`, `font-heading`).
65
+ */
66
+ declare const THEME_TOKENS: readonly ["--radius", "--font-sans", "--font-heading"];
67
+ /**
68
+ * The theme every project compiles with, beneath its own: a neutral light and
69
+ * dark palette, an 8px radius and the system sans-serif stack. A project's
70
+ * `theme.css`, its imported stylesheets and its brand layer over it.
71
+ */
72
+ declare const DEFAULT_THEME: BrandCss;
73
+ /** `DEFAULT_THEME` as theme CSS. */
74
+ declare const DEFAULT_THEME_CSS: string;
75
+ /** The fills whose text color a brand may leave out: `--color-primary` and `--color-primary-foreground`, and so on. */
76
+ declare const FILL_COLORS: readonly ["primary", "secondary", "accent"];
45
77
  /**
46
78
  * Black or white, whichever has the higher WCAG contrast ratio against
47
79
  * `color`. A translucent color is judged over white, the page an email sits on.
@@ -51,25 +83,16 @@ declare const BRAND_FOREGROUND_VARIABLE = "--color-brand-foreground";
51
83
  */
52
84
  declare const contrastForeground: (color: string) => "#000000" | "#ffffff" | undefined;
53
85
  /**
54
- * The text-on-brand colors a brand gets when it leaves them out: for the light
55
- * theme and, when the brand sets a dark-mode brand color, for dark mode. Each
56
- * is the `contrastForeground` of the brand color it sits on. A brand that sets
57
- * the foreground, or whose brand color does not evaluate, gets no default.
86
+ * The text colors a theme layer gets for the fills it sets without one: for
87
+ * each of `FILL_COLORS`, the `contrastForeground` of the fill, in light mode
88
+ * and, where the layer's dark-mode block sets the fill, in dark mode. A layer
89
+ * that sets the foreground, or whose fill does not evaluate, gets no default.
90
+ * Every build lowers each theme layer with these, so stored theme CSS never
91
+ * carries them.
58
92
  */
59
- declare const brandForegroundDefaults: (brand: BrandCss) => {
60
- readonly light?: string | undefined;
61
- readonly dark?: string | undefined;
93
+ declare const foregroundDefaults: (light: Readonly<Record<string, string>>, dark: Readonly<Record<string, string>>) => {
94
+ readonly light: Readonly<Record<string, string>>;
95
+ readonly dark: Readonly<Record<string, string>>;
62
96
  };
63
- /**
64
- * A brand theme as flat theme variables, the way a build compiles it. Email
65
- * inlining resolves `var()` once, so a dark-mode `@theme` cannot switch a
66
- * value at run time; each dark color lowers to its `-dark` theme variable,
67
- * which `dark:` utilities read (`dark:bg-brand-dark`). A text-on-brand color
68
- * the brand leaves out is filled in by `brandForegroundDefaults`, so every
69
- * build compiles the same default and the stored CSS never carries it.
70
- */
71
- declare const brandThemeVariables: (brand: BrandCss) => Readonly<Record<string, string>>;
72
- /** The stylesheet Tailwind compiles for the theme's brand import: `brandThemeVariables` and the font faces. */
73
- declare const brandTailwindCss: (brand: BrandCss) => string;
74
97
  //#endregion
75
- export { ASSET_CONTENT_TYPES, AssetEntry, AssetManifest, BRAND_COLOR_VARIABLE, BRAND_COMPONENT_CLASSES, BRAND_FOREGROUND_VARIABLE, BrandComponents, BrandCss, BrandFontFace, BrandFooterProps, BrandLogoProps, type BrandPlugin, EmailBrand, EmailBrandAddress, EmailBrandAssets, EmailBrandFooter, EmailBrandSocialLink, RenderedFooterMarkers, assetEntry, assetUrl, brandComponents, brandForegroundDefaults, brandTailwindCss, brandThemeVariables, checkAssetBase, contrastForeground, darkVariableName, isAssetPath, isExternalAsset, parseBrandCss, printBrandCss, renderedFooterMarkers, samvaBrandPlugin, withoutUnsubscribeRow };
98
+ export { ASSET_CONTENT_TYPES, AssetEntry, AssetManifest, BRAND_COMPONENT_CLASSES, BrandComponents, BrandCss, BrandFontFace, BrandFooterProps, BrandLogoProps, type BrandPlugin, DEFAULT_THEME, DEFAULT_THEME_CSS, EmailBrand, EmailBrandAddress, EmailBrandAssets, EmailBrandFooter, EmailBrandSocialLink, FILL_COLORS, RenderedFooterMarkers, THEME_COLORS, THEME_COLOR_ROLES, THEME_TOKENS, ThemeColor, assetEntry, assetUrl, brandComponents, checkAssetBase, contrastForeground, foregroundDefaults, isAssetPath, isExternalAsset, parseBrandCss, printBrandCss, renderedFooterMarkers, samvaBrandPlugin, withoutUnsubscribeRow };
package/dist/brand.js CHANGED
@@ -1,4 +1,4 @@
1
1
  import { D as samvaBrandPlugin } from "./html-eOZfDwVf.js";
2
- import { a as brandThemeVariables, c as parseBrandCss, i as brandTailwindCss, l as printBrandCss, n as BRAND_FOREGROUND_VARIABLE, o as contrastForeground, r as brandForegroundDefaults, s as darkVariableName, t as BRAND_COLOR_VARIABLE } from "./theme-css-CEUrDpqq.js";
3
- import { a as ASSET_CONTENT_TYPES, c as checkAssetBase, i as withoutUnsubscribeRow, l as isAssetPath, n as brandComponents, o as assetEntry, r as renderedFooterMarkers, s as assetUrl, t as BRAND_COMPONENT_CLASSES, u as isExternalAsset } from "./brand-C45QBKX9.js";
4
- export { ASSET_CONTENT_TYPES, BRAND_COLOR_VARIABLE, BRAND_COMPONENT_CLASSES, BRAND_FOREGROUND_VARIABLE, assetEntry, assetUrl, brandComponents, brandForegroundDefaults, brandTailwindCss, brandThemeVariables, checkAssetBase, contrastForeground, darkVariableName, isAssetPath, isExternalAsset, parseBrandCss, printBrandCss, renderedFooterMarkers, samvaBrandPlugin, withoutUnsubscribeRow };
2
+ import { a as THEME_COLOR_ROLES, c as foregroundDefaults, i as THEME_COLORS, l as parseBrandCss, n as DEFAULT_THEME_CSS, o as THEME_TOKENS, r as FILL_COLORS, s as contrastForeground, t as DEFAULT_THEME, u as printBrandCss } from "./theme-css-s9QwD5Ko.js";
3
+ import { a as ASSET_CONTENT_TYPES, c as checkAssetBase, i as withoutUnsubscribeRow, l as isAssetPath, n as brandComponents, o as assetEntry, r as renderedFooterMarkers, s as assetUrl, t as BRAND_COMPONENT_CLASSES, u as isExternalAsset } from "./brand-B-t62NJB.js";
4
+ export { ASSET_CONTENT_TYPES, BRAND_COMPONENT_CLASSES, DEFAULT_THEME, DEFAULT_THEME_CSS, FILL_COLORS, THEME_COLORS, THEME_COLOR_ROLES, THEME_TOKENS, assetEntry, assetUrl, brandComponents, checkAssetBase, contrastForeground, foregroundDefaults, isAssetPath, isExternalAsset, parseBrandCss, printBrandCss, renderedFooterMarkers, samvaBrandPlugin, withoutUnsubscribeRow };
@@ -1,8 +1,8 @@
1
1
  import { n as parseBrandSpecifier, t as BrandPlugin } from "./brand-plugin-CHhcei-j.js";
2
2
  import { a as TemplateDiagnostic, r as EmailDiagnostic } from "./diagnostics-CUYlQ_6x.js";
3
3
  import { r as EmailNode } from "./jsx-runtime-DC7xG0aA.js";
4
- import { n as AssetEntry, p as EmailBrand, u as BrandComponents } from "./assets-DCULM6QD.js";
5
- import { C as Compound, D as EmittedPosition, S as CompiledSelector, T as matchesSelector, _ as Specificity, b as parseStylesheet, ct as TemplateIr, g as CssRule, h as CssDeclaration, r as RenderedEmail, v as Stylesheet, w as classListOf, x as Ancestry, y as compileSelector } from "./render-C6OTH8BQ.js";
4
+ import { n as AssetEntry, p as EmailBrand, u as BrandComponents } from "./assets-tISHaQNF.js";
5
+ import { C as Compound, D as EmittedPosition, S as CompiledSelector, T as matchesSelector, _ as Specificity, b as parseStylesheet, ct as TemplateIr, g as CssRule, h as CssDeclaration, r as RenderedEmail, v as Stylesheet, w as classListOf, x as Ancestry, y as compileSelector } from "./render-C1X3S5or.js";
6
6
  import { a as TEMPLATE_NAME_PATTERN, n as EmailContent } from "./template-B-UDXK8a.js";
7
7
  import { caniemail } from "caniemail";
8
8
  //#region src/email/compatibility.d.ts
@@ -81,7 +81,7 @@ declare const finalizeValue: (value: string, variables: Readonly<Record<string,
81
81
  /** Media Queries Level 4 range syntax down to the `min-`/`max-` prefixes clients support. */
82
82
  declare const downlevelMediaQuery: (condition: string) => string;
83
83
  /** Render the rules that could not be inlined as one `<style>` body. */
84
- declare const renderHeadCss: (rules: readonly CssRule[], headAtRules: readonly string[], variables: Readonly<Record<string, string>>) => string;
84
+ declare const renderHeadCss: (rules: readonly CssRule[], headAtRules: readonly string[], variables: Readonly<Record<string, string>>, darkVariables?: Readonly<Record<string, string>>) => string;
85
85
  //#endregion
86
86
  //#region src/email/css-color.d.ts
87
87
  /** A color in extended sRGB, alpha included, before clamping and gamma. */
@@ -101,15 +101,24 @@ declare const downlevelColors: (value: string) => string;
101
101
  //#region src/email/cascade.d.ts
102
102
  interface AppliedStyles {
103
103
  readonly tree: EmailNode;
104
- /** Rules that could not be inlined, ready for a `<style>` element in `<head>`. */
104
+ /** Rules that could not be inlined, ready for a `<style>` element in `<head>` after `darkRules`. */
105
105
  readonly headCss: string;
106
+ /**
107
+ * A generated class for each distinct set of dark-mode declarations, mapped
108
+ * to those declarations. An element whose inlined styles read a color the
109
+ * theme's dark-mode block redefines carries the class.
110
+ */
111
+ readonly darkRules: ReadonlyMap<string, string>;
106
112
  /** Project `@font-face` rules, for a `<head>` style classic Outlook cannot see. */
107
113
  readonly fontFaceCss: string;
108
114
  readonly diagnostics: readonly EmailDiagnostic[];
109
115
  }
110
116
  /**
111
117
  * Resolve every stylesheet against the tree: inline what a client will honor on
112
- * the element, and keep the rest for `<head>`.
118
+ * the element, and keep the rest for `<head>`. Where the sheet has a dark-mode
119
+ * theme, an element whose inlined styles read a color it redefines also gets a
120
+ * generated class whose dark-mode rule restates those styles at their dark
121
+ * values.
113
122
  */
114
123
  declare const applyStylesheet: (root: EmailNode, sheet: Stylesheet, options?: {
115
124
  readonly knownClasses?: ReadonlySet<string>;
@@ -194,7 +203,10 @@ interface TransformOptions {
194
203
  }
195
204
  interface ProjectTransform {
196
205
  readonly assets: readonly AssetEntry[];
197
- /** The compiled Tailwind sheet, when the project used any utility class. */
206
+ /**
207
+ * The compiled Tailwind sheet: the default theme, the primitives' rules, the project theme and
208
+ * the utilities the project uses. Without Tailwind, the default theme and the primitives' rules.
209
+ */
198
210
  readonly tailwindSheet: Stylesheet | undefined;
199
211
  /** Every project stylesheet, parsed, by path. */
200
212
  readonly stylesheets: ReadonlyMap<string, Stylesheet>;
@@ -215,6 +227,11 @@ declare const transformProject: (files: Readonly<Record<string, string | Uint8Ar
215
227
  interface RenderedBrandFooter {
216
228
  readonly html: string;
217
229
  readonly text: string;
230
+ /**
231
+ * The rules the footer needs in the message's `<head>`: its dark-mode colors and the
232
+ * `dark:` swap of the brand logo. Empty when it needs none.
233
+ */
234
+ readonly headCss: string;
218
235
  }
219
236
  /**
220
237
  * Render `BrandFooter` against a brand on its own, for the send path to