@devdogsuga/newsletter 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/README.md +193 -0
- package/dist/assets.d.ts +54 -0
- package/dist/assets.js +42 -0
- package/dist/components.d.ts +21 -0
- package/dist/components.js +269 -0
- package/dist/darkmode.d.ts +45 -0
- package/dist/darkmode.js +138 -0
- package/dist/export/eml.d.ts +39 -0
- package/dist/export/eml.js +52 -0
- package/dist/export/index.d.ts +29 -0
- package/dist/export/index.js +82 -0
- package/dist/generated/lockup.d.ts +6 -0
- package/dist/generated/lockup.js +9 -0
- package/dist/icons.d.ts +16 -0
- package/dist/icons.js +29 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +24 -0
- package/dist/issues.d.ts +39 -0
- package/dist/issues.js +146 -0
- package/dist/theme.d.ts +123 -0
- package/dist/theme.js +155 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# @devdogsuga/newsletter
|
|
2
|
+
|
|
3
|
+
The DevDogs Changelog — the weekly newsletter — as one tree of email-safe React
|
|
4
|
+
components with two callers:
|
|
5
|
+
|
|
6
|
+
- **`apps/platform`** renders issues as pages at `/changelog` and
|
|
7
|
+
`/changelog/<version>`, straight from `ChangelogEmail`.
|
|
8
|
+
- **`@devdogsuga/devtools`** exports issues as files — `pnpm devtools
|
|
9
|
+
newsletter` — an `.html` preview and an Outlook-importable `.eml` per issue.
|
|
10
|
+
|
|
11
|
+
It deliberately does not go through `@devdogsuga/email`. That package's
|
|
12
|
+
compiled-slot pipeline exists to prove a template cannot branch on its inputs,
|
|
13
|
+
which is exactly what a newsletter must do: issues differ in how many events
|
|
14
|
+
they list. So issues here are authored data (`src/issues.ts`), rendered at
|
|
15
|
+
author time with `react-dom/server`, and the send is a finished HTML document
|
|
16
|
+
rather than a template plus fill.
|
|
17
|
+
|
|
18
|
+
## One tree, two targets
|
|
19
|
+
|
|
20
|
+
Every component takes a `RenderContext` — font stacks plus an asset resolver —
|
|
21
|
+
because the two destinations disagree about both:
|
|
22
|
+
|
|
23
|
+
- **Pages** pass `webRenderContext()`: `var(--font-display)` and friends
|
|
24
|
+
(`next/font` renames every family it self-hosts, so a literal `'Alan Sans'`
|
|
25
|
+
would never match on the site) and SVG data-URI image sources.
|
|
26
|
+
- **Exports** pass `emailRenderContext()`: literal font stacks with email-safe
|
|
27
|
+
fallbacks, and `cid:` references to PNG parts embedded in the `.eml` —
|
|
28
|
+
Gmail and both Outlooks strip SVG in any form.
|
|
29
|
+
|
|
30
|
+
Everything else is the same markup, and that markup is written for the worst
|
|
31
|
+
renderer it will meet (classic Outlook's Word engine): tables for layout,
|
|
32
|
+
longhand font properties via the `font()` helper (never the `font:`
|
|
33
|
+
shorthand), `bgColor` attributes under every painted background, no alpha
|
|
34
|
+
anywhere (`mix()` pre-blends), and button geometry on table cells rather than
|
|
35
|
+
anchors. The tests in `src/export/render.test.tsx` hold the line on the
|
|
36
|
+
mechanical parts.
|
|
37
|
+
|
|
38
|
+
## Dark mode
|
|
39
|
+
|
|
40
|
+
Dark-mode mail clients do not leave an already-dark email alone: the web
|
|
41
|
+
Outlooks (outlook.com, new Outlook, Outlook mobile) run every color through a
|
|
42
|
+
contrast-repair pass. Rendered-DOM forensics (two dumps plus a sixteen-way
|
|
43
|
+
carrier probe, 2026-09-11) showed exactly how far the 2026 transform goes: it
|
|
44
|
+
computes every element's effective background from the full cascade and
|
|
45
|
+
**injects** the repaired value inline with `!important` — which, by the
|
|
46
|
+
cascade, no stylesheet rule can ever outrank — rewrites `bgcolor` attributes
|
|
47
|
+
and border colors, deletes `background-image`, `hsl()`, `var()` and
|
|
48
|
+
`color-mix()` from the embedded stylesheet, and recolors `rgba()` in place.
|
|
49
|
+
Exactly one paint mechanism passed the probe untouched, inline and in the
|
|
50
|
+
stylesheet alike: `box-shadow`. The architecture follows from those findings:
|
|
51
|
+
|
|
52
|
+
- **No background is ever inline.** Every painted element carries a `bc-`
|
|
53
|
+
class naming its hex; `paintCss()` in `src/darkmode.ts` supplies the base
|
|
54
|
+
rule (the color plus `solidBg`'s same-color `linear-gradient` underlay, as
|
|
55
|
+
Gmail armor), and a `bgcolor` attribute covers clients without `<style>`.
|
|
56
|
+
The dot-grid and slant textures ride the same stylesheet as `.bg-dots` /
|
|
57
|
+
`.bg-slants`. The platform's `/changelog` pages embed `paintCss()` too.
|
|
58
|
+
- **Pins re-assert every color** (`darkModeCss()`): each value repeated with
|
|
59
|
+
`!important` under `@media (prefers-color-scheme: dark)` for clients that
|
|
60
|
+
honor it, and under the `[data-ogsc]`/`[data-ogsb]` attributes the web
|
|
61
|
+
Outlooks stamp on elements they recolor — rules in that scope escape their
|
|
62
|
+
color conversion (ancestor-scoped single-selector rules only; that is the
|
|
63
|
+
one shape Outlook.com's CSS support keeps).
|
|
64
|
+
- **The scoped pins carry the real weapon:** a same-color
|
|
65
|
+
`box-shadow: inset 0 0 0 3000px` on every `bc-` pin, painting the authored
|
|
66
|
+
color over the gray Outlook injected. It lives only under the `data-og*`
|
|
67
|
+
scopes because an inset shadow paints over `background-image` — in any
|
|
68
|
+
other client it would erase the bar textures, but those scopes apply
|
|
69
|
+
exactly when Outlook has already stripped the textures anyway.
|
|
70
|
+
- **One deliberate sacrifice:** `<body>` keeps its inline background so the
|
|
71
|
+
web Outlooks repaint _it_, stamp it with `data-ogsb`, and thereby switch on
|
|
72
|
+
every scoped pin below. The repainted body is fully covered by the pinned
|
|
73
|
+
full-width table, so the sacrifice never shows.
|
|
74
|
+
|
|
75
|
+
Text colors stay inline (with `tc-` classes alongside); Outlook's text
|
|
76
|
+
repairs — lightening the dim grays — read fine on the armored surfaces.
|
|
77
|
+
Borders split by role: Outlook repaints border colors inline with
|
|
78
|
+
`!important` too, so every structural divider is a painted 1px cell
|
|
79
|
+
(`DividerCell`, full background armor) and only the card outlines remain
|
|
80
|
+
true borders, where the repaint reads as an intentional outline. In its dark
|
|
81
|
+
mode the design trades its textures and the featured card's block shadow for
|
|
82
|
+
flat, correct surfaces. A render test walks every issue and fails on any
|
|
83
|
+
inline background outside `<body>`, any `bgcolor` without its class, and any
|
|
84
|
+
class without its paint rule and pin.
|
|
85
|
+
|
|
86
|
+
None of this survives being sent from an Outlook composer — see
|
|
87
|
+
[Sending](#sending). The defenses assume the authored document reaches
|
|
88
|
+
recipients intact, which only `--send` guarantees.
|
|
89
|
+
|
|
90
|
+
The one holdout is classic Outlook for Windows in dark mode: the Word engine
|
|
91
|
+
does a full color invert with no override hook. The result stays readable
|
|
92
|
+
(its whole purpose is contrast), just re-themed, and readers get a
|
|
93
|
+
per-message toggle back to the sent colors.
|
|
94
|
+
|
|
95
|
+
## Issues
|
|
96
|
+
|
|
97
|
+
`src/issues.ts` is the content: one `ChangelogIssue` per send, versioned with
|
|
98
|
+
semver because a send is a release. The version doubles as the archive URL
|
|
99
|
+
segment and the export filename. A copy pass before a send touches that one
|
|
100
|
+
file; the platform archive and the exported email can never disagree.
|
|
101
|
+
|
|
102
|
+
## Exporting
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
pnpm --filter @devdogsuga/newsletter build # the CLI consumes dist/
|
|
106
|
+
pnpm devtools newsletter # pick issues interactively
|
|
107
|
+
pnpm devtools newsletter '*' --out ~/changelog
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The `.eml` carries `X-Unsent: 1` and no `Message-ID`, so **classic Outlook
|
|
111
|
+
for Windows** opens it as an editable compose draft. New Outlook and Outlook
|
|
112
|
+
on the web do not honor `X-Unsent` and open `.eml` files read-only; for those
|
|
113
|
+
clients the draft has to already be in the mailbox — which is what `--push`
|
|
114
|
+
does. Either way the draft is a review copy: the send happens with `--send`
|
|
115
|
+
(below), because every Outlook composer rewrites what it sends.
|
|
116
|
+
|
|
117
|
+
## Pushing a draft
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
pnpm devtools newsletter 3.0.1 --push
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
appends the issue — same MIME as the `.eml`, minus `X-Unsent` — straight into
|
|
124
|
+
the Drafts folder of `devdogs@uga.edu` over IMAP, where an officer can review
|
|
125
|
+
it in any Outlook signed into the club account.
|
|
126
|
+
|
|
127
|
+
Review is all a draft is for. **Never send the newsletter from Outlook
|
|
128
|
+
itself.** Its composers are rich-text editors with a style whitelist, and
|
|
129
|
+
sending re-serializes whatever the editor kept: forensics on a received copy
|
|
130
|
+
showed the head `<style>` replaced with Outlook's own, every
|
|
131
|
+
`background-image` and `bgcolor` stripped, and most class attributes dropped —
|
|
132
|
+
the entire dark-mode defense, gone in transit, leaving recipients' dark-mode
|
|
133
|
+
Outlooks free to repaint every background. (Classic Outlook mangles
|
|
134
|
+
differently, into Word HTML, with the same result.)
|
|
135
|
+
|
|
136
|
+
## Sending
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
pnpm devtools newsletter 3.0.1 --send --to listserv@listserv.uga.edu
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
submits the issue over SMTP as the club mailbox, byte-for-byte as authored —
|
|
143
|
+
no composer touches it, so recipients get the document with its stylesheet,
|
|
144
|
+
pins and gradient underlays intact. `--to` is required and comma-separated;
|
|
145
|
+
there is deliberately no interactive path to a send.
|
|
146
|
+
|
|
147
|
+
The first `--push` or `--send` opens a browser; sign in as the mailbox and
|
|
148
|
+
the CLI catches the redirect itself on a loopback port. (If no local port
|
|
149
|
+
would bind, it falls back to printing the URL and asking for the
|
|
150
|
+
`https://localhost` address the browser lands on.) The refresh token is
|
|
151
|
+
stored at `~/.config/devdogsuga/newsletter-mailbox.json` (mode 600 — it opens
|
|
152
|
+
the club mailbox, treat it like a password) and every later run is silent.
|
|
153
|
+
`--mailbox` overrides the account.
|
|
154
|
+
|
|
155
|
+
Why IMAP/SMTP and not Microsoft Graph: UGA's tenant blocks user consent for
|
|
156
|
+
every Graph mail scope, but Microsoft's default consent policy allowlists a
|
|
157
|
+
handful of mail clients by application ID for the legacy IMAP and SMTP
|
|
158
|
+
scopes. The sign-in therefore presents Thunderbird's public client ID — the
|
|
159
|
+
same grant Thunderbird itself would hold, and a documented convention in
|
|
160
|
+
open-source mail tooling (mbsync, OfflineIMAP, DavMail) — but it is
|
|
161
|
+
Microsoft's allowlist, and this stops working the day they prune it. The
|
|
162
|
+
durable fix is an EITS-approved app registration with delegated
|
|
163
|
+
`Mail.ReadWrite` and `Mail.Send`; if that ever lands, swap the IMAP APPEND
|
|
164
|
+
and SMTP submission for their Graph calls and delete the borrowed ID.
|
|
165
|
+
|
|
166
|
+
## Regenerating the lockup
|
|
167
|
+
|
|
168
|
+
`src/generated/lockup.ts` embeds `apps/platform/public/brand/devdogs-logo-dark.svg`
|
|
169
|
+
(the composed mascot + wordmark, which `@devdogsuga/og` does not export). If
|
|
170
|
+
that artwork changes, re-run from the repo root:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
node --input-type=module -e '
|
|
174
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
175
|
+
const svg = readFileSync("apps/platform/public/brand/devdogs-logo-dark.svg", "utf8").trim();
|
|
176
|
+
const [, w, h] = svg.match(/viewBox="0 0 ([\d.]+) ([\d.]+)"/);
|
|
177
|
+
writeFileSync("packages/newsletter/src/generated/lockup.ts", [
|
|
178
|
+
"// @generated from apps/platform/public/brand/devdogs-logo-dark.svg — the",
|
|
179
|
+
"// mascot + wordmark lockup drawn for dark grounds. Regenerate by re-running",
|
|
180
|
+
"// the base64 line in packages/newsletter/README.md if the artwork changes.",
|
|
181
|
+
"",
|
|
182
|
+
"/** The artwork's own viewBox units, carried so display sizes derive an aspect ratio instead of hard-coding one. */",
|
|
183
|
+
"export const DEVDOGS_LOCKUP_ON_DARK = {",
|
|
184
|
+
` src: "data:image/svg+xml;base64,${Buffer.from(svg).toString("base64")}",`,
|
|
185
|
+
` width: ${w},`,
|
|
186
|
+
` height: ${h},`,
|
|
187
|
+
"} as const;",
|
|
188
|
+
"",
|
|
189
|
+
].join("\n"));
|
|
190
|
+
'
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
[API reference](https://devdogsuga.org/docs/toolkit/reference/api/newsletter)
|
package/dist/assets.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two brand marks the newsletter draws: the DevDogs mascot+wordmark
|
|
3
|
+
* lockup (embedded here — brand carries the wordmark and mascot separately,
|
|
4
|
+
* and the masthead wants the composed artwork) and the GDG-on-Campus · UGA
|
|
5
|
+
* cobrand, taken from brand so the two stay one file.
|
|
6
|
+
*
|
|
7
|
+
* Display sizes are derived from each artwork's own units against a shared
|
|
8
|
+
* 33px masthead height, so a re-exported SVG with a new viewBox cannot
|
|
9
|
+
* silently stretch.
|
|
10
|
+
*/
|
|
11
|
+
import { GDGC_UGA, type Asset } from "@devdogsuga/brand";
|
|
12
|
+
import { type SocialIconName } from "./icons.js";
|
|
13
|
+
import type { FontStacks } from "./theme.js";
|
|
14
|
+
export { GDGC_UGA };
|
|
15
|
+
export declare const DEVDOGS_LOCKUP: Asset;
|
|
16
|
+
/** On-screen `<img>` dimensions, shared by the page, the email and the PNG rasteriser (which renders at 2x these). */
|
|
17
|
+
export declare const MARK_SIZES: {
|
|
18
|
+
readonly devdogsLockup: {
|
|
19
|
+
readonly width: number;
|
|
20
|
+
readonly height: 33;
|
|
21
|
+
};
|
|
22
|
+
readonly gdgcLockup: {
|
|
23
|
+
readonly width: number;
|
|
24
|
+
readonly height: 33;
|
|
25
|
+
};
|
|
26
|
+
readonly socialIcon: {
|
|
27
|
+
readonly width: 17;
|
|
28
|
+
readonly height: 17;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
/** Where the components find their three `<img>` sources. */
|
|
32
|
+
export interface NewsletterAssets {
|
|
33
|
+
/** `<img>` src for the DevDogs mascot+wordmark lockup (dark ground). */
|
|
34
|
+
devdogsLockup: string;
|
|
35
|
+
/** `<img>` src for the GDG On Campus · UGA cobrand lockup (dark ground). */
|
|
36
|
+
gdgcLockup: string;
|
|
37
|
+
/** `<img>` src for a footer social icon. */
|
|
38
|
+
socialIcon: (name: SocialIconName) => string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Everything a render target decides: the page passes `next/font` CSS
|
|
42
|
+
* variables and SVG sources, the exporter passes literal font stacks and
|
|
43
|
+
* `cid:` references to embedded PNGs.
|
|
44
|
+
*/
|
|
45
|
+
export interface RenderContext {
|
|
46
|
+
fonts: FontStacks;
|
|
47
|
+
assets: NewsletterAssets;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The marks as self-contained data URIs — right for anything with a real
|
|
51
|
+
* browser behind it (the platform pages, an .html preview on disk), and
|
|
52
|
+
* exactly wrong for a send: Gmail and Outlook strip SVG in any form.
|
|
53
|
+
*/
|
|
54
|
+
export declare const DATA_URI_ASSETS: NewsletterAssets;
|
package/dist/assets.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two brand marks the newsletter draws: the DevDogs mascot+wordmark
|
|
3
|
+
* lockup (embedded here — brand carries the wordmark and mascot separately,
|
|
4
|
+
* and the masthead wants the composed artwork) and the GDG-on-Campus · UGA
|
|
5
|
+
* cobrand, taken from brand so the two stay one file.
|
|
6
|
+
*
|
|
7
|
+
* Display sizes are derived from each artwork's own units against a shared
|
|
8
|
+
* 33px masthead height, so a re-exported SVG with a new viewBox cannot
|
|
9
|
+
* silently stretch.
|
|
10
|
+
*/
|
|
11
|
+
import { GDGC_UGA } from "@devdogsuga/brand";
|
|
12
|
+
import { DEVDOGS_LOCKUP_ON_DARK } from "./generated/lockup.js";
|
|
13
|
+
import { socialIconDataUri } from "./icons.js";
|
|
14
|
+
export { GDGC_UGA };
|
|
15
|
+
export const DEVDOGS_LOCKUP = DEVDOGS_LOCKUP_ON_DARK;
|
|
16
|
+
/** The masthead/footer line height every mark is scaled to. */
|
|
17
|
+
const MARK_HEIGHT = 33;
|
|
18
|
+
function displayWidth(asset) {
|
|
19
|
+
return Math.round((MARK_HEIGHT * asset.width) / asset.height);
|
|
20
|
+
}
|
|
21
|
+
/** On-screen `<img>` dimensions, shared by the page, the email and the PNG rasteriser (which renders at 2x these). */
|
|
22
|
+
export const MARK_SIZES = {
|
|
23
|
+
devdogsLockup: {
|
|
24
|
+
width: displayWidth(DEVDOGS_LOCKUP),
|
|
25
|
+
height: MARK_HEIGHT,
|
|
26
|
+
},
|
|
27
|
+
gdgcLockup: {
|
|
28
|
+
width: displayWidth(GDGC_UGA),
|
|
29
|
+
height: MARK_HEIGHT,
|
|
30
|
+
},
|
|
31
|
+
socialIcon: { width: 17, height: 17 },
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* The marks as self-contained data URIs — right for anything with a real
|
|
35
|
+
* browser behind it (the platform pages, an .html preview on disk), and
|
|
36
|
+
* exactly wrong for a send: Gmail and Outlook strip SVG in any form.
|
|
37
|
+
*/
|
|
38
|
+
export const DATA_URI_ASSETS = {
|
|
39
|
+
devdogsLockup: DEVDOGS_LOCKUP.src,
|
|
40
|
+
gdgcLockup: GDGC_UGA.src,
|
|
41
|
+
socialIcon: socialIconDataUri,
|
|
42
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type RenderContext } from "./assets.js";
|
|
2
|
+
import type { ChangelogIssue } from "./issues.js";
|
|
3
|
+
/**
|
|
4
|
+
* The complete issue, from the terminal title bar to the footer lockups —
|
|
5
|
+
* everything inside (and including) the 600px shell.
|
|
6
|
+
*/
|
|
7
|
+
export declare function ChangelogEmail({ issue, ctx, }: {
|
|
8
|
+
issue: ChangelogIssue;
|
|
9
|
+
ctx: RenderContext;
|
|
10
|
+
}): import("react").JSX.Element;
|
|
11
|
+
/**
|
|
12
|
+
* The full mail document around `ChangelogEmail`: head metas, the Google Fonts
|
|
13
|
+
* link (honored by Apple/iOS Mail, ignored by Gmail and Outlook, which fall
|
|
14
|
+
* down the literal stacks), the hidden inbox-preview line, and the centered
|
|
15
|
+
* canvas. Only the exporter renders this; the platform pages already have a
|
|
16
|
+
* document of their own.
|
|
17
|
+
*/
|
|
18
|
+
export declare function ChangelogDocument({ issue, ctx, }: {
|
|
19
|
+
issue: ChangelogIssue;
|
|
20
|
+
ctx: RenderContext;
|
|
21
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { MARK_SIZES } from "./assets.js";
|
|
3
|
+
import { bc, brc, darkModeCss, DOT_GRID_CLASS, paintCss, SLANTS_CLASS, tc, } from "./darkmode.js";
|
|
4
|
+
import { blockShadow, chipColors, font, HEADING_TINTS, KIND, PALETTE, SITE, SOCIAL_LINKS, solidBg, FONTS_HREF, UGA, } from "./theme.js";
|
|
5
|
+
const WIDTH = 600;
|
|
6
|
+
/** Every layout table shares these; email clients honor the attributes where they ignore the CSS. */
|
|
7
|
+
const TABLE_RESET = {
|
|
8
|
+
role: "presentation",
|
|
9
|
+
cellPadding: 0,
|
|
10
|
+
cellSpacing: 0,
|
|
11
|
+
border: 0,
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* React 19's types dropped the legacy `bgcolor` from `<td>` (it survives on
|
|
15
|
+
* `<table>`). Classic Outlook still reads the attribute where it ignores the
|
|
16
|
+
* CSS, so it arrives through a spread the checker cannot veto; React renders
|
|
17
|
+
* lowercase unknown attributes verbatim.
|
|
18
|
+
*/
|
|
19
|
+
function bgAttr(color) {
|
|
20
|
+
return { bgcolor: color };
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A 1px divider as a painted cell, never a border: the web Outlooks' dark
|
|
24
|
+
* mode repaints border colors inline with `!important` (turning dividers
|
|
25
|
+
* near-white on the pinned dark surfaces), while a painted cell gets the
|
|
26
|
+
* full background armor. Card outlines stay true borders — their repaint
|
|
27
|
+
* reads as an intentional outline.
|
|
28
|
+
*/
|
|
29
|
+
function DividerCell({ vertical = false }) {
|
|
30
|
+
const size = vertical ? { width: "1px" } : { height: "1px" };
|
|
31
|
+
return (_jsx("td", { ...bgAttr(PALETTE.border), ...(vertical ? { width: 1 } : { height: 1 }), className: bc(PALETTE.border), style: { ...size, fontSize: "0px", lineHeight: 0 }, children: " " }));
|
|
32
|
+
}
|
|
33
|
+
/** Three title-bar dots in event-type colors, echoing terminal window controls. */
|
|
34
|
+
function KindDots() {
|
|
35
|
+
return (_jsx("span", { style: { fontSize: "0px", lineHeight: 0 }, children: [KIND.interest, KIND.build, KIND.study].map((color) => (_jsx("span", { className: bc(color), style: {
|
|
36
|
+
display: "inline-block",
|
|
37
|
+
width: "11px",
|
|
38
|
+
height: "11px",
|
|
39
|
+
borderRadius: "50%",
|
|
40
|
+
marginLeft: "6px",
|
|
41
|
+
} }, color))) }));
|
|
42
|
+
}
|
|
43
|
+
/** The site's event chip: a tinted pill with a faint accent border and lightened accent text, flattened to hex. */
|
|
44
|
+
function Chip({ label, color, ground = PALETTE.card, fonts, }) {
|
|
45
|
+
const colors = chipColors(color, ground);
|
|
46
|
+
return (_jsx("span", { className: `${tc(colors.text)} ${bc(colors.fill)} ${brc(colors.border)}`, style: {
|
|
47
|
+
display: "inline-block",
|
|
48
|
+
border: `1px solid ${colors.border}`,
|
|
49
|
+
color: colors.text,
|
|
50
|
+
...font(700, 11, 1, fonts.sans),
|
|
51
|
+
letterSpacing: "0.4px",
|
|
52
|
+
textTransform: "uppercase",
|
|
53
|
+
padding: "5px 10px",
|
|
54
|
+
borderRadius: "20px",
|
|
55
|
+
}, children: label }));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Outlook's Word engine does not reliably apply padding, borders, or
|
|
59
|
+
* backgrounds to anchors. All button geometry lives on a table cell; the
|
|
60
|
+
* anchor is responsible only for the label.
|
|
61
|
+
*/
|
|
62
|
+
function CtaButton({ label, url, color, ground, fonts, compact = false, }) {
|
|
63
|
+
const fill = ground ?? color;
|
|
64
|
+
return (_jsx("table", { ...TABLE_RESET, children: _jsx("tbody", { children: _jsx("tr", { children: _jsx("td", { align: "center", valign: "middle", ...bgAttr(fill), className: `${bc(fill)} ${brc(color)}`, style: {
|
|
65
|
+
border: `1px solid ${color}`,
|
|
66
|
+
borderRadius: "8px",
|
|
67
|
+
padding: compact ? "9px 13px" : "14px 26px",
|
|
68
|
+
msoPaddingAlt: "0",
|
|
69
|
+
whiteSpace: "nowrap",
|
|
70
|
+
}, children: _jsxs("a", { href: url, className: tc(PALETTE.ink), style: {
|
|
71
|
+
display: "block",
|
|
72
|
+
...font(compact ? 700 : 800, compact ? 12 : 15, 1, fonts.sans),
|
|
73
|
+
color: PALETTE.ink,
|
|
74
|
+
textDecoration: "none",
|
|
75
|
+
msoLineHeightRule: "exactly",
|
|
76
|
+
}, children: [label, " ", "\u25B8"] }) }) }) }) }));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* An upcoming-event row, drawn the way the site's schedule list draws one: a
|
|
80
|
+
* stacked weekday / day-number date column, a chip pill, a display-face
|
|
81
|
+
* title, and the time + room in quiet sans.
|
|
82
|
+
*/
|
|
83
|
+
function EventRow({ event, ctx, }) {
|
|
84
|
+
const { fonts } = ctx;
|
|
85
|
+
const [month, day] = event.date.split(" ");
|
|
86
|
+
return (_jsx("table", { ...TABLE_RESET, width: "100%", bgcolor: PALETTE.card, className: `${bc(PALETTE.card)} ${brc(PALETTE.border)}`, style: {
|
|
87
|
+
border: `1px solid ${PALETTE.border}`,
|
|
88
|
+
borderRadius: "10px",
|
|
89
|
+
margin: "0 0 12px",
|
|
90
|
+
}, children: _jsx("tbody", { children: _jsxs("tr", { children: [_jsxs("td", { width: 66, align: "center", valign: "middle", style: {
|
|
91
|
+
padding: "16px 2px 16px 8px",
|
|
92
|
+
}, children: [_jsx("div", { className: tc(PALETTE.mute), style: {
|
|
93
|
+
...font(800, 10, 1, fonts.display),
|
|
94
|
+
letterSpacing: "2px",
|
|
95
|
+
color: PALETTE.mute,
|
|
96
|
+
textTransform: "uppercase",
|
|
97
|
+
}, children: event.dow }), _jsx("div", { className: tc(PALETTE.ink), style: {
|
|
98
|
+
...font(800, 28, 1.1, fonts.display),
|
|
99
|
+
color: PALETTE.ink,
|
|
100
|
+
fontVariantNumeric: "tabular-nums",
|
|
101
|
+
padding: "3px 0 2px",
|
|
102
|
+
}, children: day }), _jsx("div", { className: tc(PALETTE.dim), style: {
|
|
103
|
+
...font(700, 10, 1, fonts.display),
|
|
104
|
+
letterSpacing: "2px",
|
|
105
|
+
color: PALETTE.dim,
|
|
106
|
+
textTransform: "uppercase",
|
|
107
|
+
}, children: month })] }), _jsx(DividerCell, { vertical: true }), _jsx("td", { valign: "middle", style: { padding: "15px 17px" }, children: _jsx("table", { ...TABLE_RESET, width: "100%", children: _jsx("tbody", { children: _jsxs("tr", { children: [_jsxs("td", { valign: "middle", children: [_jsx(Chip, { label: event.chip, color: event.color, fonts: fonts }), _jsx("div", { className: tc(PALETTE.ink), style: {
|
|
108
|
+
...font(800, 18, 1.25, fonts.display),
|
|
109
|
+
color: PALETTE.ink,
|
|
110
|
+
letterSpacing: "-0.2px",
|
|
111
|
+
padding: "9px 0 5px",
|
|
112
|
+
}, children: event.title }), _jsxs("div", { className: tc(PALETTE.mute), style: {
|
|
113
|
+
...font(400, 13, 1.5, fonts.sans),
|
|
114
|
+
color: PALETTE.mute,
|
|
115
|
+
}, children: [event.time, " ", _jsx("span", { className: tc(PALETTE.dim), style: { color: PALETTE.dim }, children: "\u00B7" }), " ", event.loc] }), event.blurb ? (_jsx("div", { className: tc(PALETTE.dim), style: {
|
|
116
|
+
...font(400, 13, 1.55, fonts.sans),
|
|
117
|
+
color: PALETTE.dim,
|
|
118
|
+
paddingTop: "7px",
|
|
119
|
+
}, children: event.blurb })) : null] }), event.rsvp ? (_jsx("td", { valign: "middle", align: "right", width: 74, style: { paddingLeft: "10px" }, children: _jsx(CtaButton, { label: "RSVP", url: event.rsvp, color: event.color, ground: PALETTE.card, fonts: fonts, compact: true }) })) : null] }) }) }) })] }) }) }));
|
|
120
|
+
}
|
|
121
|
+
/** A `## heading` in the terminal voice. */
|
|
122
|
+
function SectionHeading({ children, color, fonts, paddingBottom = 14, }) {
|
|
123
|
+
return (_jsxs("div", { className: tc(color), style: {
|
|
124
|
+
...font(700, 13, 1, fonts.mono),
|
|
125
|
+
color,
|
|
126
|
+
padding: `0 0 ${paddingBottom}px`,
|
|
127
|
+
}, children: ["## ", children] }));
|
|
128
|
+
}
|
|
129
|
+
/** One quiet bottom card — the shared frame under "new here?" and "sign-off". */
|
|
130
|
+
function QuietCard({ children }) {
|
|
131
|
+
return (_jsx("table", { ...TABLE_RESET, width: "100%", bgcolor: PALETTE.card, className: `${bc(PALETTE.card)} ${brc(PALETTE.border)}`, style: {
|
|
132
|
+
border: `1px solid ${PALETTE.border}`,
|
|
133
|
+
borderRadius: "10px",
|
|
134
|
+
}, children: _jsx("tbody", { children: _jsx("tr", { children: _jsx("td", { style: { padding: "18px 20px" }, children: children }) }) }) }));
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The complete issue, from the terminal title bar to the footer lockups —
|
|
138
|
+
* everything inside (and including) the 600px shell.
|
|
139
|
+
*/
|
|
140
|
+
export function ChangelogEmail({ issue, ctx, }) {
|
|
141
|
+
const { fonts, assets } = ctx;
|
|
142
|
+
const featured = issue.featured;
|
|
143
|
+
const arrowLinkStyle = {
|
|
144
|
+
color: PALETTE.ink,
|
|
145
|
+
textDecoration: "none",
|
|
146
|
+
};
|
|
147
|
+
return (_jsx("table", { ...TABLE_RESET, width: WIDTH, bgcolor: PALETTE.bg, className: `${bc(PALETTE.bg)} ${brc(PALETTE.border)}`, style: {
|
|
148
|
+
maxWidth: `${WIDTH}px`,
|
|
149
|
+
width: "100%",
|
|
150
|
+
border: `1px solid ${PALETTE.border}`,
|
|
151
|
+
borderRadius: "14px",
|
|
152
|
+
overflow: "hidden",
|
|
153
|
+
}, children: _jsxs("tbody", { children: [_jsx("tr", { children: _jsx("td", { ...bgAttr(PALETTE.bar), className: `${bc(PALETTE.bar)} ${DOT_GRID_CLASS}`, style: {
|
|
154
|
+
padding: "12px 18px",
|
|
155
|
+
}, children: _jsx("table", { ...TABLE_RESET, width: "100%", children: _jsx("tbody", { children: _jsxs("tr", { children: [_jsx("td", { width: 86, children: _jsx(KindDots, {}) }), _jsxs("td", { align: "center", className: tc(PALETTE.dim), style: {
|
|
156
|
+
...font(600, 12, 1, fonts.mono),
|
|
157
|
+
color: PALETTE.dim,
|
|
158
|
+
}, children: ["devdogs -", " ", _jsx("span", { className: tc(KIND.workshop), style: { color: KIND.workshop }, children: "changelog" }), " ", "- 80\u00D724"] }), _jsx("td", { width: 86, align: "right", className: tc(PALETTE.dim), style: {
|
|
159
|
+
...font(600, 12, 1, fonts.mono),
|
|
160
|
+
color: PALETTE.dim,
|
|
161
|
+
}, children: issue.sendLabel })] }) }) }) }) }), _jsx("tr", { children: _jsx(DividerCell, {}) }), _jsx("tr", { children: _jsxs("td", { style: { padding: "24px 22px 4px" }, children: [_jsx("table", { ...TABLE_RESET, width: "100%", children: _jsx("tbody", { children: _jsxs("tr", { children: [_jsx("td", { valign: "middle", children: _jsx("table", { ...TABLE_RESET, children: _jsx("tbody", { children: _jsxs("tr", { children: [_jsx("td", { valign: "middle", children: _jsx("img", { src: assets.devdogsLockup, width: MARK_SIZES.devdogsLockup.width, height: MARK_SIZES.devdogsLockup.height, alt: "DevDogs", style: { display: "block" } }) }), _jsx("td", { valign: "bottom", style: { padding: "0 0 5px 8px" }, children: _jsx("span", { className: tc(KIND.workshop), style: {
|
|
162
|
+
...font(700, 22, 1, fonts.mono),
|
|
163
|
+
letterSpacing: "-0.4px",
|
|
164
|
+
color: KIND.workshop,
|
|
165
|
+
}, children: "Changelog_" }) })] }) }) }) }), _jsxs("td", { valign: "middle", align: "right", children: [_jsxs("div", { className: tc(UGA), style: { ...font(800, 12, 1, fonts.mono), color: UGA }, children: ["v", issue.version] }), _jsx("div", { className: tc(PALETTE.mute), style: {
|
|
166
|
+
...font(600, 12, 1, fonts.mono),
|
|
167
|
+
color: PALETTE.mute,
|
|
168
|
+
paddingTop: "5px",
|
|
169
|
+
}, children: issue.term })] })] }) }) }), _jsxs("div", { className: tc(PALETTE.mute), style: {
|
|
170
|
+
...font(400, 13, 1.6, fonts.mono),
|
|
171
|
+
color: PALETTE.mute,
|
|
172
|
+
paddingTop: "16px",
|
|
173
|
+
}, children: [_jsx("span", { className: tc(KIND.workshop), style: { color: KIND.workshop, fontWeight: 700 }, children: "$" }), " ", _jsx("span", { className: tc(KIND.workshop), style: { color: KIND.workshop }, children: "changelog" }), " ", _jsx("span", { className: tc(PALETTE.ink), style: { color: PALETTE.ink }, children: issue.command.replace(/^changelog\s*/, "") })] }), _jsxs("div", { className: tc(PALETTE.dim), style: {
|
|
174
|
+
...font(400, 13, 1.6, fonts.mono),
|
|
175
|
+
color: PALETTE.dim,
|
|
176
|
+
paddingTop: "3px",
|
|
177
|
+
}, children: ["\u2192 mirror:", " ", _jsxs("a", { href: `${SITE}/changelog/${issue.version}`, className: tc(KIND.build), style: { color: KIND.build, textDecoration: "none" }, children: ["curl devdogsuga.org/changelog/", issue.version] })] })] }) }), _jsx("tr", { children: _jsx("td", { style: { padding: "14px 22px 0" }, children: _jsx("table", { ...TABLE_RESET, width: "100%", children: _jsx("tbody", { children: _jsx("tr", { children: _jsx("td", { ...bgAttr(UGA), className: `${bc(UGA)} ${SLANTS_CLASS}`, height: 9, style: {
|
|
178
|
+
height: "9px",
|
|
179
|
+
borderRadius: "4px",
|
|
180
|
+
fontSize: "0px",
|
|
181
|
+
lineHeight: 0,
|
|
182
|
+
}, children: " " }) }) }) }) }) }), _jsx("tr", { children: _jsxs("td", { style: { padding: "20px 22px 6px" }, children: [_jsx("div", { className: tc(PALETTE.ink), style: {
|
|
183
|
+
...font(800, 24, 1.25, fonts.display),
|
|
184
|
+
color: PALETTE.ink,
|
|
185
|
+
letterSpacing: "-0.3px",
|
|
186
|
+
}, children: issue.tagline }), _jsx("div", { className: tc(PALETTE.mute), style: {
|
|
187
|
+
...font(400, 15, 1.6, fonts.sans),
|
|
188
|
+
color: PALETTE.mute,
|
|
189
|
+
paddingTop: "11px",
|
|
190
|
+
}, children: issue.intro })] }) }), _jsx("tr", { children: _jsxs("td", { style: { padding: "20px 22px 6px" }, children: [_jsx(SectionHeading, { color: featured.color, fonts: fonts, children: issue.featuredLabel }), _jsx("table", { ...TABLE_RESET, width: "100%", bgcolor: PALETTE.card2, className: `${bc(PALETTE.card2)} ${brc(featured.color)}`, style: {
|
|
191
|
+
border: `1px solid ${featured.color}`,
|
|
192
|
+
borderRadius: "10px",
|
|
193
|
+
...blockShadow(featured.color),
|
|
194
|
+
}, children: _jsx("tbody", { children: _jsx("tr", { children: _jsxs("td", { style: { padding: "22px 22px 24px" }, children: [_jsx(Chip, { label: featured.chip, color: featured.color, ground: PALETTE.card2, fonts: fonts }), _jsx("div", { className: tc(PALETTE.ink), style: {
|
|
195
|
+
...font(700, 30, 1.1, fonts.display),
|
|
196
|
+
color: PALETTE.ink,
|
|
197
|
+
letterSpacing: "-0.4px",
|
|
198
|
+
padding: "14px 0 8px",
|
|
199
|
+
}, children: featured.title }), _jsx("table", { ...TABLE_RESET, style: { margin: "2px 0 4px" }, children: _jsx("tbody", { children: _jsxs("tr", { children: [_jsxs("td", { className: tc(PALETTE.ink), style: {
|
|
200
|
+
...font(700, 14, 1.5, fonts.sans),
|
|
201
|
+
color: PALETTE.ink,
|
|
202
|
+
paddingRight: "8px",
|
|
203
|
+
}, children: [featured.dow, ", ", featured.date] }), _jsx(DividerCell, { vertical: true }), _jsx("td", { className: tc(PALETTE.mute), style: {
|
|
204
|
+
...font(400, 14, 1.5, fonts.sans),
|
|
205
|
+
color: PALETTE.mute,
|
|
206
|
+
padding: "0 8px",
|
|
207
|
+
}, children: featured.time }), _jsx(DividerCell, { vertical: true }), _jsx("td", { className: tc(PALETTE.mute), style: {
|
|
208
|
+
...font(400, 14, 1.5, fonts.sans),
|
|
209
|
+
color: PALETTE.mute,
|
|
210
|
+
padding: "0 8px",
|
|
211
|
+
}, children: featured.loc })] }) }) }), _jsx("div", { className: tc(PALETTE.mute), style: {
|
|
212
|
+
...font(400, 15, 1.6, fonts.sans),
|
|
213
|
+
color: PALETTE.mute,
|
|
214
|
+
paddingTop: "10px",
|
|
215
|
+
}, children: featured.blurb }), _jsx("div", { style: {
|
|
216
|
+
height: "18px",
|
|
217
|
+
lineHeight: "18px",
|
|
218
|
+
fontSize: "0px",
|
|
219
|
+
}, children: " " }), _jsx(CtaButton, { label: issue.cta, url: featured.rsvp ?? `${SITE}/events`, color: UGA, fonts: fonts })] }) }) }) })] }) }), _jsx("tr", { children: _jsxs("td", { style: { padding: "22px 22px 0" }, children: [_jsx(SectionHeading, { color: PALETTE.ink, fonts: fonts, children: "upcoming" }), issue.upcoming.map((event, index) => (_jsx(EventRow, { event: event, ctx: ctx }, `${event.date}-${index}`))), _jsx("div", { style: { padding: "2px 0 4px", textAlign: "center" }, children: _jsx("a", { href: `${SITE}/events`, className: tc(KIND.build), style: {
|
|
220
|
+
...font(700, 13, 1, fonts.mono),
|
|
221
|
+
color: KIND.build,
|
|
222
|
+
textDecoration: "none",
|
|
223
|
+
}, children: "\u2192 full calendar & subscribe (.ics)" }) })] }) }), _jsx("tr", { children: _jsx("td", { style: { padding: "18px 22px 0" }, children: _jsxs(QuietCard, { children: [_jsx(SectionHeading, { color: HEADING_TINTS.newHere, fonts: fonts, paddingBottom: 11, children: "new here?" }), _jsx("div", { className: tc(PALETTE.mute), style: {
|
|
224
|
+
...font(400, 14, 1.6, fonts.sans),
|
|
225
|
+
color: PALETTE.mute,
|
|
226
|
+
paddingBottom: "12px",
|
|
227
|
+
}, children: "No experience required, ever. Bring a laptop if you've got one, curiosity if you don't. Start here:" }), _jsxs("div", { className: tc(PALETTE.ink), style: { ...font(600, 14, 2, fonts.mono), color: PALETTE.ink }, children: [_jsxs("a", { href: "https://discord.gg/devdogs", className: tc(PALETTE.ink), style: arrowLinkStyle, children: [_jsx("span", { className: tc(KIND.build), style: { color: KIND.build }, children: "\u2192" }), " ", "join the Discord"] }), _jsx("br", {}), _jsxs("a", { href: `${SITE}/events`, className: tc(PALETTE.ink), style: arrowLinkStyle, children: [_jsx("span", { className: tc(KIND.workshop), style: { color: KIND.workshop }, children: "\u2192" }), " ", "add our events to your calendar"] }), _jsx("br", {}), _jsxs("a", { href: "https://github.com/devdogsuga", className: tc(PALETTE.ink), style: arrowLinkStyle, children: [_jsx("span", { className: tc(KIND.social), style: { color: KIND.social }, children: "\u2192" }), " ", "browse what members are building"] })] })] }) }) }), _jsx("tr", { children: _jsx("td", { style: { padding: "18px 22px 24px" }, children: _jsxs(QuietCard, { children: [_jsx(SectionHeading, { color: HEADING_TINTS.signoff, fonts: fonts, paddingBottom: 11, children: "sign-off" }), _jsxs("div", { className: tc(PALETTE.mute), style: {
|
|
228
|
+
...font(400, 14, 1.6, fonts.sans),
|
|
229
|
+
color: PALETTE.mute,
|
|
230
|
+
}, children: [issue.signoff, _jsx("div", { className: tc(PALETTE.dim), style: {
|
|
231
|
+
color: PALETTE.dim,
|
|
232
|
+
fontSize: "13px",
|
|
233
|
+
textAlign: "right",
|
|
234
|
+
paddingTop: "4px",
|
|
235
|
+
}, children: "— the DevDogs officers" })] })] }) }) }), _jsx("tr", { children: _jsx(DividerCell, {}) }), _jsx("tr", { children: _jsxs("td", { align: "center", ...bgAttr(PALETTE.bar), className: `${bc(PALETTE.bar)} ${DOT_GRID_CLASS}`, style: {
|
|
236
|
+
padding: "28px 22px 30px",
|
|
237
|
+
}, children: [_jsx("table", { ...TABLE_RESET, style: { margin: "0 auto" }, children: _jsx("tbody", { children: _jsxs("tr", { children: [_jsx("td", { align: "center", valign: "top", style: { padding: "0 20px" }, children: _jsx("img", { src: assets.devdogsLockup, width: MARK_SIZES.devdogsLockup.width, height: MARK_SIZES.devdogsLockup.height, alt: "DevDogs", style: { display: "block" } }) }), _jsx("td", { align: "center", valign: "top", style: { padding: "7px 20px 0" }, children: _jsx("img", { src: assets.gdgcLockup, width: MARK_SIZES.gdgcLockup.width, height: MARK_SIZES.gdgcLockup.height, alt: "Google Developer Groups On Campus \u00B7 University of Georgia", style: { display: "block" } }) })] }) }) }), _jsx("table", { ...TABLE_RESET, style: { margin: "20px auto 0" }, children: _jsx("tbody", { children: _jsx("tr", { children: SOCIAL_LINKS.map((social) => (_jsx("td", { align: "center", valign: "middle", style: { padding: "0 10px", whiteSpace: "nowrap" }, children: _jsxs("a", { href: social.url, className: tc(PALETTE.ink), style: {
|
|
238
|
+
...font(600, 13, "17px", fonts.mono),
|
|
239
|
+
color: PALETTE.ink,
|
|
240
|
+
textDecoration: "none",
|
|
241
|
+
whiteSpace: "nowrap",
|
|
242
|
+
}, children: [_jsx("img", { src: assets.socialIcon(social.icon), width: MARK_SIZES.socialIcon.width, height: MARK_SIZES.socialIcon.height, alt: "", style: {
|
|
243
|
+
display: "inline-block",
|
|
244
|
+
verticalAlign: "middle",
|
|
245
|
+
border: 0,
|
|
246
|
+
outline: "none",
|
|
247
|
+
} }), " ", _jsx("span", { className: tc(PALETTE.ink), style: { color: PALETTE.ink }, children: social.label })] }) }, social.label))) }) }) }), _jsxs("div", { className: tc(PALETTE.dim), style: {
|
|
248
|
+
...font(400, 12, 1.7, fonts.mono),
|
|
249
|
+
color: PALETTE.dim,
|
|
250
|
+
paddingTop: "16px",
|
|
251
|
+
}, children: ["DevDogs at the University of Georgia", _jsx("br", {}), "Google Developer Group on Campus"] })] }) })] }) }));
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* The full mail document around `ChangelogEmail`: head metas, the Google Fonts
|
|
255
|
+
* link (honored by Apple/iOS Mail, ignored by Gmail and Outlook, which fall
|
|
256
|
+
* down the literal stacks), the hidden inbox-preview line, and the centered
|
|
257
|
+
* canvas. Only the exporter renders this; the platform pages already have a
|
|
258
|
+
* document of their own.
|
|
259
|
+
*/
|
|
260
|
+
export function ChangelogDocument({ issue, ctx, }) {
|
|
261
|
+
return (_jsxs("html", { lang: "en", children: [_jsxs("head", { children: [_jsx("meta", { charSet: "utf-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }), _jsx("meta", { name: "x-apple-disable-message-reformatting" }), _jsx("meta", { name: "color-scheme", content: "light dark" }), _jsx("meta", { name: "supported-color-schemes", content: "light dark" }), _jsx("title", { children: issue.title }), _jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }), _jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" }), _jsx("link", { href: FONTS_HREF, rel: "stylesheet" }), _jsx("style", { dangerouslySetInnerHTML: { __html: `@import url('${FONTS_HREF}');` } }), _jsx("style", { dangerouslySetInnerHTML: {
|
|
262
|
+
__html: `${paintCss()}\n${darkModeCss()}`,
|
|
263
|
+
} })] }), _jsxs("body", { className: bc(PALETTE.bg), ...bgAttr(PALETTE.bg), style: { margin: 0, padding: 0, ...solidBg(PALETTE.bg) }, children: [_jsx("div", { style: {
|
|
264
|
+
display: "none",
|
|
265
|
+
maxHeight: 0,
|
|
266
|
+
overflow: "hidden",
|
|
267
|
+
opacity: 0,
|
|
268
|
+
}, children: issue.preview }), _jsx("table", { ...TABLE_RESET, width: "100%", bgcolor: PALETTE.bg, className: bc(PALETTE.bg), children: _jsx("tbody", { children: _jsx("tr", { children: _jsx("td", { align: "center", style: { padding: "24px 12px" }, children: _jsx(ChangelogEmail, { issue: issue, ctx: ctx }) }) }) }) })] })] }));
|
|
269
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Class for an element whose `color` is `color`. */
|
|
2
|
+
export declare function tc(color: string): string;
|
|
3
|
+
/** Class for an element whose background is `color`. */
|
|
4
|
+
export declare function bc(color: string): string;
|
|
5
|
+
/** Class for an element with a `color`-colored border. */
|
|
6
|
+
export declare function brc(color: string): string;
|
|
7
|
+
/** The slant stripe's texture class — always the UGA red. */
|
|
8
|
+
export declare const SLANTS_CLASS = "bg-slants";
|
|
9
|
+
/** The terminal bars' faint dot-grid texture class. */
|
|
10
|
+
export declare const DOT_GRID_CLASS = "bg-dots";
|
|
11
|
+
/**
|
|
12
|
+
* The base paint layer: what actually colors every `bc-` background, since no
|
|
13
|
+
* background is ever inline (see the module comment). Deliberately without
|
|
14
|
+
* `!important` so the pins in `darkModeCss()` outrank it, and with the
|
|
15
|
+
* texture rules last so their `background-image` wins the tie against the
|
|
16
|
+
* flat `bc-` rule on the same element. The platform's /changelog pages embed
|
|
17
|
+
* this too — the classes paint there exactly as they do in an inbox.
|
|
18
|
+
*/
|
|
19
|
+
export declare function paintCss(): string;
|
|
20
|
+
/**
|
|
21
|
+
* The pinning stylesheet `ChangelogDocument` embeds. Three layers, weakest
|
|
22
|
+
* first: a `color-scheme` declaration telling well-behaved clients the email
|
|
23
|
+
* handles both schemes itself, the same pins under `prefers-color-scheme` for
|
|
24
|
+
* clients that honor the media query (Apple Mail, Outlook for Mac), and the
|
|
25
|
+
* `data-ogsc`/`data-ogsb` scoped pins for the web Outlooks, which apply their
|
|
26
|
+
* recoloring in the DOM rather than through any media query.
|
|
27
|
+
*
|
|
28
|
+
* The scoped layers are where the real fight happens. The 2026 web Outlooks
|
|
29
|
+
* compute every element's effective background from the full cascade and
|
|
30
|
+
* inject the repaired value inline with `!important` — unbeatable by any
|
|
31
|
+
* stylesheet color — so the scoped pins carry the box-shadow armor (see
|
|
32
|
+
* `rules`) that repaints the authored color on top. The color pins still
|
|
33
|
+
* matter for Outlook mobile's older transform, and the `<body>` donor in
|
|
34
|
+
* `ChangelogDocument` guarantees a stamped ancestor so the scope always
|
|
35
|
+
* matches in dark mode.
|
|
36
|
+
*
|
|
37
|
+
* The scoped rules are deliberately spartan: Outlook.com's CSS support only
|
|
38
|
+
* stretches to an attribute selector on an ancestor (`[data-ogsc] .x`), not
|
|
39
|
+
* compounded onto the element itself (`.x[data-ogsc]`), and a selector its
|
|
40
|
+
* sanitizer rejects can take the whole rule down with it — so every pin is
|
|
41
|
+
* its own single-selector rule, in the one shape the client is documented
|
|
42
|
+
* to keep. Both attributes scope every rule because which one Outlook stamps
|
|
43
|
+
* depends on whether it rewrote a color or a background up the tree.
|
|
44
|
+
*/
|
|
45
|
+
export declare function darkModeCss(): string;
|