@company-semantics/contracts 36.0.0 → 38.0.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/package.json +1 -1
- package/src/notifications/renderers/email/README.md +42 -15
- package/src/notifications/renderers/email/__tests__/amp.test.ts +13 -5
- package/src/notifications/renderers/email/colors.ts +30 -15
- package/src/notifications/renderers/email/index.ts +15 -5
- package/src/notifications/renderers/email/shells.ts +42 -19
package/package.json
CHANGED
|
@@ -50,11 +50,31 @@ The cost is that `EmailLine.html` is now an intermediate representation —
|
|
|
50
50
|
`render.ts` no longer reads as the bytes it emits, and the snapshot is where the
|
|
51
51
|
real markup is legible.
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
**
|
|
57
|
-
|
|
53
|
+
### ⚠️ The AMP body is NEVER SENT
|
|
54
|
+
|
|
55
|
+
Two of the three ship. `amp` is rendered, previewed in the Ladle gallery, and
|
|
56
|
+
dropped — and **delivering it is a regression, not a feature**. The backend's
|
|
57
|
+
`EmailDelivery.test.ts` fails if it reaches the transport; that test is the
|
|
58
|
+
invariant, this section is its explanation.
|
|
59
|
+
|
|
60
|
+
**Why not sent.** Gmail iOS inverts the `text/html` part post-delivery and does NOT
|
|
61
|
+
touch a `text/x-amp-html` part. That inversion is the only dark mode a Gmail iOS
|
|
62
|
+
reader gets — no Gmail client honours `prefers-color-scheme` on any surface — so an
|
|
63
|
+
AMP part would switch it off and hand them a white email in a dark client. AMP buys
|
|
64
|
+
`:hover` in Gmail and would cost dark mode in Gmail. Measured, not argued:
|
|
65
|
+
`company-semantics-backend/docs/dark-mode-probe.md` (ADR-CONTRACTS-091, -092).
|
|
66
|
+
|
|
67
|
+
**Why it exists.** AMP is the only email format with a real animation model —
|
|
68
|
+
`amp-animation`, `amp-bind`, `amp-position-observer`, no arbitrary JS. Nothing else
|
|
69
|
+
can make the kaomoji companions blink once, three seconds after open. Keeping the
|
|
70
|
+
surface alive costs one inert field; rebuilding it later costs more. If Google ever
|
|
71
|
+
allows `prefers-color-scheme` here, or Gmail starts inverting AMP too, the trade
|
|
72
|
+
flips and this is ready.
|
|
73
|
+
|
|
74
|
+
**Light-only, permanently.** `prefers-color-scheme` in `<style amp-custom>` is a hard
|
|
75
|
+
validator error (explicit 14-feature allowlist, frozen since 2023, unchanged by
|
|
76
|
+
`data-css-strict`). The `media` ATTRIBUTE takes the same feature, PASSES the
|
|
77
|
+
validator — and Gmail's runtime ignores it. Both doors closed.
|
|
58
78
|
|
|
59
79
|
`pnpm validate:amp` runs the real AMP validator. `__tests__/amp.test.ts` asserts
|
|
60
80
|
the constraints we KNOW on every run, which is not the same claim: it passed once
|
|
@@ -161,12 +181,15 @@ expect to style it properly rather than trust the placeholder.
|
|
|
161
181
|
simply rests. It is also the ONE rule the AMP surface shares, via
|
|
162
182
|
`hoverStyle(false)`; AMP forbids `!important`, which is why that template takes a
|
|
163
183
|
parameter and `DARK_STYLE` does not.
|
|
164
|
-
- The AMP surface has NO dark scheme, and
|
|
165
|
-
the `prefers-color-scheme` media feature (
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
184
|
+
- The AMP surface has NO dark scheme, and that is why it is never SENT. AMP4EMAIL
|
|
185
|
+
disallows the `prefers-color-scheme` media feature (an explicit 14-feature
|
|
186
|
+
allowlist, unchanged by `data-css-strict`), so it cannot have one. ADR-CONTRACTS-089
|
|
187
|
+
called that free — "AMP's readers force-invert and never got our dark colours
|
|
188
|
+
anyway". Half right: they never got them, because no Gmail client honours the media
|
|
189
|
+
feature on any surface. But Gmail web recolours nothing, Yahoo does not invert, and
|
|
190
|
+
Gmail iOS inverts the html part while EXEMPTING an AMP part — so an AMP part does
|
|
191
|
+
not lose a scheme its readers never had, it switches off the inversion that IS
|
|
192
|
+
their dark mode. Measured; see the AMP section above.
|
|
170
193
|
- Role hooks ride the AMP surface unstyled (`cs-faint`, `cs-bubble`, `cs-cta`,
|
|
171
194
|
`cs-link`), because their only rules live in `DARK_STYLE`. Their colours arrive
|
|
172
195
|
via each recipe's `roleDecl`, so this is dead bytes rather than a bug — the price
|
|
@@ -183,7 +206,11 @@ expect to style it properly rather than trust the placeholder.
|
|
|
183
206
|
promise: made alone it stops Apple Mail protecting our colours while leaving
|
|
184
207
|
light text on a dark background — worse than declaring light-only. This channel
|
|
185
208
|
WAS light-only by construction until ADR-CONTRACTS-088; it is not any more.
|
|
186
|
-
- Dark reaches Apple Mail, iOS Mail and Outlook for Mac. Gmail
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
209
|
+
- Dark reaches Apple Mail, iOS Mail and Outlook for Mac, and nobody else. Gmail
|
|
210
|
+
honours none of it on any surface, so a Gmail reader's dark mode is whatever their
|
|
211
|
+
client does to our light colours unaided: Gmail iOS inverts the html part, and
|
|
212
|
+
**Gmail web does nothing at all** — a light email in a dark client. That gap is
|
|
213
|
+
real and open, and the lever is inversion-indifferent authoring in `colors.ts`
|
|
214
|
+
(mid-tones, no pure `#ffffff`/`#000000` in large fields, solid-fill CTAs), not
|
|
215
|
+
another surface. The dark path is additive: a client that strips `<style>` keeps
|
|
216
|
+
the inline light colours, which is the status quo rather than a degradation.
|
|
@@ -174,11 +174,19 @@ describe("the AMP surface", () => {
|
|
|
174
174
|
|
|
175
175
|
it("carries no dark scheme, because AMP disallows the media feature", () => {
|
|
176
176
|
// NOT an omission to fix later: `prefers-color-scheme` is a disallowed media
|
|
177
|
-
// feature in amp-custom (
|
|
178
|
-
// `data-css-strict
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
// the
|
|
177
|
+
// feature in amp-custom (an explicit 14-feature allowlist in the validator's
|
|
178
|
+
// protoascii, `issues_as_error: true`, unchanged by `data-css-strict`, frozen
|
|
179
|
+
// since 2023), so this surface cannot have dark in any form.
|
|
180
|
+
//
|
|
181
|
+
// Nor can it via the back door: the `media` ATTRIBUTE on an AMP element takes
|
|
182
|
+
// the same feature and PASSES the validator — and Gmail's runtime ignores it,
|
|
183
|
+
// rendering a dark-gated and a light-gated block at once. Measured; see
|
|
184
|
+
// `company-semantics-backend/docs/dark-mode-probe.md`. Both doors are closed.
|
|
185
|
+
//
|
|
186
|
+
// The surface being light-only is exactly why it is never SENT: an AMP part
|
|
187
|
+
// switches off Gmail iOS's inversion, which is the only dark mode those readers
|
|
188
|
+
// get (ADR-CONTRACTS-091/-092). It is rendered for a future that needs AMP's
|
|
189
|
+
// animation model, not for delivery.
|
|
182
190
|
//
|
|
183
191
|
// This test exists so that "add dark mode to AMP" fails here with the reason,
|
|
184
192
|
// rather than in `pnpm validate:amp` with a CSS syntax error.
|
|
@@ -270,12 +270,16 @@ const dark = PALETTES.dark;
|
|
|
270
270
|
* the LIGHT hover; the dark block follows it in the shell and overrides it, so
|
|
271
271
|
* source order is what resolves them (same specificity).
|
|
272
272
|
*
|
|
273
|
-
* Whether any of it happens is the client's call
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
273
|
+
* Whether any of it happens is the client's call. Apple Mail honours `:hover`, new
|
|
274
|
+
* Outlook for Mac and Yahoo partially, Gmail and classic Outlook not at all. There
|
|
275
|
+
* is no fallback to write: without it the button rests, which is how it looks
|
|
276
|
+
* today. Purely additive, like the dark path.
|
|
277
|
+
*
|
|
278
|
+
* This is the ONE role whose reach the AMP surface would change — Gmail honours
|
|
279
|
+
* `:hover` there — and it is why that surface exists. It is NOT why the surface is
|
|
280
|
+
* unsent: shipping it would cost Gmail iOS its dark mode, which is worth more than
|
|
281
|
+
* a hover state (ADR-CONTRACTS-091/-092). The `important` parameter is what lets
|
|
282
|
+
* one rule serve both shells; see `./shells.ts`.
|
|
279
283
|
*
|
|
280
284
|
* `.cs-cta-hover` is present ONLY on a CTA that actually has an `href` — see
|
|
281
285
|
* `./cta.ts`. An OTP code wears `.cs-cta` for its edge and never this, because a
|
|
@@ -301,15 +305,26 @@ export const BASE_STYLE = hoverStyle(true);
|
|
|
301
305
|
* colour is inline, inline beats a stylesheet on specificity, and without
|
|
302
306
|
* `!important` every rule here silently loses and dark quietly does nothing.
|
|
303
307
|
*
|
|
304
|
-
* **This block is the html surface's alone, and
|
|
305
|
-
* AMP4EMAIL disallows the `prefers-color-scheme` media feature
|
|
306
|
-
*
|
|
307
|
-
* surface cannot carry these rules in any form
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
* this
|
|
308
|
+
* **This block is the html surface's alone, and that is why the AMP surface is
|
|
309
|
+
* never sent.** AMP4EMAIL disallows the `prefers-color-scheme` media feature
|
|
310
|
+
* outright — a hard validator error, checked with and without `data-css-strict` —
|
|
311
|
+
* so the AMP surface cannot carry these rules in any form (ADR-CONTRACTS-089).
|
|
312
|
+
* Unlike `hoverStyle`, there was no version of this worth parameterizing.
|
|
313
|
+
*
|
|
314
|
+
* ADR-CONTRACTS-089 called that free, on the grounds that AMP's readers force-invert
|
|
315
|
+
* and never received these colours anyway. **The first half is true and the second is
|
|
316
|
+
* false.** Nobody in Gmail or Yahoo honours `prefers-color-scheme`, so this block
|
|
317
|
+
* genuinely never reached them — but Gmail web recolours nothing, Yahoo does not
|
|
318
|
+
* invert, and Gmail iOS inverts the html part while EXEMPTING an AMP part. So an AMP
|
|
319
|
+
* part does not lose a scheme its readers never got; it switches off the client-side
|
|
320
|
+
* inversion that IS their dark mode. Measured in
|
|
321
|
+
* `company-semantics-backend/docs/dark-mode-probe.md`; the surface stays unsent
|
|
322
|
+
* (ADR-CONTRACTS-091/-092).
|
|
323
|
+
*
|
|
324
|
+
* **Which leaves a real gap this block cannot close.** Gmail web recolours nothing
|
|
325
|
+
* and honours nothing, so a light email stays light in a dark client. The lever there
|
|
326
|
+
* is inversion-indifferent authoring in this file — mid-tones, no pure `#ffffff` /
|
|
327
|
+
* `#000000` in large fields, solid-fill CTAs — not another surface.
|
|
313
328
|
*
|
|
314
329
|
* `.cs-faint` states both `color` and `border-top-color` because one role lands
|
|
315
330
|
* on two properties — the `/* EOM *\/` span takes the first and ignores the
|
|
@@ -33,9 +33,11 @@ import { ampShell, type EmailLine, htmlShell, textShell } from "./shells";
|
|
|
33
33
|
* Three bodies, because an email IS a `multipart/alternative`: one message, whose
|
|
34
34
|
* parts a client chooses between. This is NOT three channels — a recipient gets one
|
|
35
35
|
* email — which is why AMP lives here rather than behind a second `Renderer`
|
|
36
|
-
* (ADR-CONTRACTS-089).
|
|
37
|
-
*
|
|
38
|
-
*
|
|
36
|
+
* (ADR-CONTRACTS-089).
|
|
37
|
+
*
|
|
38
|
+
* **Two of them ship.** Assembling the parts is delivery's job, and delivery is not
|
|
39
|
+
* contracts' — but the `amp` body is not merely un-plumbed, it is deliberately unsent
|
|
40
|
+
* and must stay that way (ADR-CONTRACTS-092). See the field.
|
|
39
41
|
*/
|
|
40
42
|
export interface RenderedEmail {
|
|
41
43
|
/** Subject line — the notification's `metadata.title`. */
|
|
@@ -45,8 +47,16 @@ export interface RenderedEmail {
|
|
|
45
47
|
/** HTML body (`text/html`) — every kind is multi-output. */
|
|
46
48
|
html: string;
|
|
47
49
|
/**
|
|
48
|
-
* AMP4EMAIL body (`text/x-amp-html`)
|
|
49
|
-
*
|
|
50
|
+
* AMP4EMAIL body (`text/x-amp-html`).
|
|
51
|
+
*
|
|
52
|
+
* ⚠️ **Rendered, and deliberately NEVER SENT** (ADR-CONTRACTS-092). Delivering it
|
|
53
|
+
* is a regression: Gmail iOS inverts the html part and does NOT touch an AMP
|
|
54
|
+
* part, so an AMP part switches off the only dark mode a Gmail iOS reader gets.
|
|
55
|
+
* `EmailDelivery.test.ts` in the backend fails if this reaches the transport.
|
|
56
|
+
*
|
|
57
|
+
* It is kept alive for AMP's animation model (`amp-animation`, `amp-bind`),
|
|
58
|
+
* which no other email format has, and which is the one thing that could make it
|
|
59
|
+
* worth sending. See `./shells.ts`'s `ampShell`.
|
|
50
60
|
*/
|
|
51
61
|
amp: string;
|
|
52
62
|
}
|
|
@@ -49,10 +49,17 @@ export interface EmailLine {
|
|
|
49
49
|
* text colour sitting on top of it. The two ship together or not at all.
|
|
50
50
|
*
|
|
51
51
|
* Only Apple Mail, iOS Mail and Outlook for Mac read any of this. Clients that
|
|
52
|
-
*
|
|
53
|
-
* and rewrite the inline styles post-delivery
|
|
54
|
-
* is why the body still states an explicit
|
|
55
|
-
* default.
|
|
52
|
+
* recolour instead (Outlook.com, Windows Outlook, and — measured —
|
|
53
|
+
* **Gmail iOS**) ignore both metas and rewrite the inline styles post-delivery.
|
|
54
|
+
* Nothing here can stop that, which is why the body still states an explicit
|
|
55
|
+
* background rather than trusting a default.
|
|
56
|
+
*
|
|
57
|
+
* Do not read that as a degradation to fix: Gmail iOS's inversion of THIS part is
|
|
58
|
+
* the only dark mode a Gmail iOS reader gets, and preserving it is why `ampShell`'s
|
|
59
|
+
* output is never sent (ADR-CONTRACTS-092). Gmail web, by contrast, recolours
|
|
60
|
+
* nothing at all — a light email stays light in a dark client, and no surface fixes
|
|
61
|
+
* that. Gmail Android is untested: unknown, not negative.
|
|
62
|
+
* `company-semantics-backend/docs/dark-mode-probe.md`.
|
|
56
63
|
*
|
|
57
64
|
* The `<!--[if mso]>` block is the mirror image: it is read ONLY by the Outlook
|
|
58
65
|
* Word engine, and undoes the one layout that engine cannot render — a linked
|
|
@@ -86,12 +93,38 @@ ${inner}
|
|
|
86
93
|
}
|
|
87
94
|
|
|
88
95
|
/**
|
|
89
|
-
* Wrap rendered lines in the AMP4EMAIL shell (ADR-CONTRACTS-089).
|
|
96
|
+
* Wrap rendered lines in the AMP4EMAIL shell (ADR-CONTRACTS-089, -091, -092).
|
|
97
|
+
*
|
|
98
|
+
* ⚠️ **THIS BODY IS NEVER SENT, AND MUST NOT BE.** It is rendered, previewed in the
|
|
99
|
+
* Ladle gallery, and dropped. Delivering it is a REGRESSION, not a feature — see
|
|
100
|
+
* below. `company-semantics-backend`'s `EmailDelivery.test.ts` fails if it ever
|
|
101
|
+
* reaches the transport, and that test is the invariant; this comment is only its
|
|
102
|
+
* explanation.
|
|
103
|
+
*
|
|
104
|
+
* **Why it is not sent.** Gmail iOS inverts the `text/html` part post-delivery and
|
|
105
|
+
* does **NOT** touch a `text/x-amp-html` part. That inversion is the ONLY dark mode
|
|
106
|
+
* a Gmail iOS reader gets — no Gmail client honours `prefers-color-scheme` on any
|
|
107
|
+
* surface — so shipping an AMP part would switch it off and hand them a white email
|
|
108
|
+
* in a dark client. This surface buys `:hover` in Gmail and would cost dark mode in
|
|
109
|
+
* Gmail. That trade was measured and refused (ADR-CONTRACTS-091), and the measurement
|
|
110
|
+
* is `company-semantics-backend/docs/dark-mode-probe.md`. ADR-CONTRACTS-089 asserted
|
|
111
|
+
* the opposite — "AMP clients force-invert, so they never got our dark colours
|
|
112
|
+
* anyway" — and that is FALSE: Gmail web recolours nothing, Yahoo does not invert,
|
|
113
|
+
* and Gmail iOS exempts AMP.
|
|
90
114
|
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
115
|
+
* **Why it exists at all, then.** AMP is the only email format with a real animation
|
|
116
|
+
* model — `amp-animation`, `amp-bind`, `amp-position-observer` — and no arbitrary JS
|
|
117
|
+
* needed. Nothing else can make the kaomoji companions blink once, three seconds
|
|
118
|
+
* after open. That is a future worth keeping the surface alive for, and rebuilding it
|
|
119
|
+
* later costs more than carrying it inert now (ADR-CONTRACTS-092). If Google ever
|
|
120
|
+
* allows `prefers-color-scheme` here, or Gmail starts inverting AMP too, the trade
|
|
121
|
+
* flips and this is ready.
|
|
122
|
+
*
|
|
123
|
+
* **Light-only, permanently.** AMP4EMAIL disallows the `prefers-color-scheme` media
|
|
124
|
+
* feature — a hard validator error, checked with and without `data-css-strict` — so
|
|
125
|
+
* `DARK_STYLE` cannot appear here in any form. The `media` ATTRIBUTE on an AMP
|
|
126
|
+
* element takes the same feature and PASSES the validator; Gmail's runtime ignores
|
|
127
|
+
* it. Both doors are closed. That is not a gap to close; it is the format.
|
|
95
128
|
*
|
|
96
129
|
* Everything AMP forbids is absent BY CONSTRUCTION rather than by stripping:
|
|
97
130
|
*
|
|
@@ -103,16 +136,6 @@ ${inner}
|
|
|
103
136
|
* in a line, so nothing has to remove it. Outlook is not an AMP client, and
|
|
104
137
|
* `display: block` needs no correction in the three that are.
|
|
105
138
|
*
|
|
106
|
-
* **No dark scheme, and that is not an omission.** AMP4EMAIL disallows the
|
|
107
|
-
* `prefers-color-scheme` media feature — verified against the AMP validator, with
|
|
108
|
-
* and without `data-css-strict` — so `DARK_STYLE` cannot appear here in any form.
|
|
109
|
-
* It costs nothing: that block is read by Apple Mail, iOS Mail and Outlook for Mac,
|
|
110
|
-
* and this surface is read by Gmail, Yahoo and Mail.ru. The two sets are disjoint,
|
|
111
|
-
* and the AMP clients force-invert rather than honour a stylesheet — so they were
|
|
112
|
-
* never receiving our dark colours on the html surface either. What this surface
|
|
113
|
-
* adds is `:hover`, which those clients DO honour and which the html surface cannot
|
|
114
|
-
* reach them with. It trades nothing away.
|
|
115
|
-
*
|
|
116
139
|
* The boilerplate hides the body until the runtime unhides it — which is why an
|
|
117
140
|
* AMP document that cannot run scripts shows nothing at all, rather than showing
|
|
118
141
|
* an unstyled version of itself.
|