@company-semantics/contracts 33.0.0 → 35.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 +4 -4
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +2 -2
- package/src/index.ts +17 -11
- package/src/notifications/README.md +140 -0
- package/src/notifications/__tests__/README.md +54 -0
- package/src/notifications/__tests__/__snapshots__/README.md +32 -0
- package/src/notifications/__tests__/content.test.ts +186 -0
- package/src/notifications/__tests__/context.test.ts +72 -0
- package/src/notifications/__tests__/definition.test.ts +222 -0
- package/src/notifications/__tests__/kinds.test.ts +81 -0
- package/src/notifications/__tests__/output-parity.golden.ts +363 -0
- package/src/notifications/__tests__/output-parity.test.ts +122 -0
- package/src/notifications/__tests__/registry.test.ts +184 -0
- package/src/{email/render → notifications}/__tests__/render-snapshot.test.ts +58 -29
- package/src/notifications/__tests__/renderer.test.ts +181 -0
- package/src/notifications/content.ts +249 -0
- package/src/notifications/context.ts +70 -0
- package/src/notifications/definition.ts +82 -0
- package/src/notifications/index.ts +104 -0
- package/src/notifications/kinds/README.md +57 -0
- package/src/notifications/kinds/auth-otp.ts +99 -0
- package/src/notifications/kinds/chat-shared.ts +56 -0
- package/src/notifications/kinds/company-md-access-approved.ts +57 -0
- package/src/notifications/kinds/company-md-access-denied.ts +61 -0
- package/src/notifications/kinds/company-md-access-requested.ts +65 -0
- package/src/notifications/kinds/index.ts +19 -0
- package/src/notifications/kinds/org-invite.ts +62 -0
- package/src/notifications/kinds/ownership-transfer-completed.ts +57 -0
- package/src/notifications/kinds/ownership-transfer.ts +68 -0
- package/src/notifications/kinds/security-alert.ts +78 -0
- package/src/notifications/kinds/share-granted.ts +74 -0
- package/src/notifications/kinds/unit-owner-granted.ts +95 -0
- package/src/notifications/kinds.ts +68 -0
- package/src/{email/types.ts → notifications/payloads.ts} +33 -68
- package/src/notifications/registry.ts +107 -0
- package/src/notifications/render.ts +106 -0
- package/src/notifications/renderer.ts +50 -0
- package/src/notifications/renderers/README.md +50 -0
- package/src/notifications/renderers/email/README.md +74 -0
- package/src/notifications/renderers/email/__tests__/README.md +29 -0
- package/src/notifications/renderers/email/__tests__/render.test.ts +180 -0
- package/src/{email/render → notifications/renderers/email}/chat.ts +53 -107
- package/src/notifications/renderers/email/constants.ts +47 -0
- package/src/notifications/renderers/email/cta.ts +63 -0
- package/src/{email/render → notifications/renderers/email}/escape-html.ts +5 -1
- package/src/notifications/renderers/email/index.ts +73 -0
- package/src/notifications/renderers/email/render.ts +225 -0
- package/src/notifications/renderers/email/shells.ts +60 -0
- package/src/notifications/renderers/slack/README.md +73 -0
- package/src/notifications/renderers/slack/__tests__/README.md +33 -0
- package/src/notifications/renderers/slack/__tests__/index.test.ts +201 -0
- package/src/notifications/renderers/slack/index.ts +261 -0
- package/src/notifications/renderers/sms/README.md +69 -0
- package/src/notifications/renderers/sms/__tests__/README.md +31 -0
- package/src/notifications/renderers/sms/__tests__/index.test.ts +162 -0
- package/src/notifications/renderers/sms/index.ts +131 -0
- package/src/notifications/text.ts +52 -0
- package/src/email/README.md +0 -51
- package/src/email/__tests__/registry.test.ts +0 -161
- package/src/email/index.ts +0 -36
- package/src/email/registry.ts +0 -155
- package/src/email/render/auth-otp.ts +0 -71
- package/src/email/render/blocks.ts +0 -281
- package/src/email/render/chat-shared.ts +0 -35
- package/src/email/render/company-md-access-approved.ts +0 -42
- package/src/email/render/company-md-access-denied.ts +0 -43
- package/src/email/render/company-md-access-requested.ts +0 -46
- package/src/email/render/constants.ts +0 -18
- package/src/email/render/index.ts +0 -58
- package/src/email/render/org-invite.ts +0 -40
- package/src/email/render/ownership-transfer-completed.ts +0 -41
- package/src/email/render/ownership-transfer.ts +0 -42
- package/src/email/render/render-email.ts +0 -194
- package/src/email/render/security-alert.ts +0 -61
- package/src/email/render/share-granted.ts +0 -52
- package/src/email/render/unit-owner-granted.ts +0 -60
- /package/src/{email/render → notifications}/__tests__/__snapshots__/render-snapshot.test.ts.snap +0 -0
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared email building blocks — dual-output components.
|
|
3
|
-
*
|
|
4
|
-
* Every component returns a `Block` (`{ html, text }`), so a template composes
|
|
5
|
-
* ONE list of blocks and both surfaces derive from the same source. Principle:
|
|
6
|
-
* **blocks own the styling; templates supply only content — the only allowable
|
|
7
|
-
* UI is the components.** Editing a block restyles every email, HTML and plain
|
|
8
|
-
* text, across the backend (real sends) and the app (Ladle preview).
|
|
9
|
-
*
|
|
10
|
-
* This module holds the primitives — shells, paragraphs, and the CTA box. The
|
|
11
|
-
* chat unit builds on them from `./chat`; both are re-exported together from
|
|
12
|
-
* `./index`, which is the surface templates and consumers import.
|
|
13
|
-
*
|
|
14
|
-
* INVARIANTS:
|
|
15
|
-
* - Pure functions, no side effects — except `signature()`, which reads the
|
|
16
|
-
* current year (`new Date().getFullYear()`) for the copyright line.
|
|
17
|
-
* - Components escape their own content; templates pass raw text (+ `bold(...)`
|
|
18
|
-
* for inline emphasis). No template hand-writes markup or raw strings.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { COMPANY_NAME, MONO_FONT_STACK } from "./constants";
|
|
22
|
-
import { escapeHtml } from "./escape-html";
|
|
23
|
-
|
|
24
|
-
/** Shared type styling. Exported for `./chat` only — not part of the package
|
|
25
|
-
* surface (`./index` does not re-export these), so components stay the one
|
|
26
|
-
* place styling is decided. */
|
|
27
|
-
export const MONO = `font-family: ${MONO_FONT_STACK};`;
|
|
28
|
-
|
|
29
|
-
/** The single font size for every email element (HTML). Plain text is monospace
|
|
30
|
-
* so it carries no size — this keeps one visual size across both surfaces. */
|
|
31
|
-
export const FONT_SIZE = "13px";
|
|
32
|
-
|
|
33
|
-
// =============================================================================
|
|
34
|
-
// Core types
|
|
35
|
-
// =============================================================================
|
|
36
|
-
|
|
37
|
-
/** A rendered block — both presentations of one component. `spacing` controls
|
|
38
|
-
* the plain-text gap AFTER this block ("normal" = blank line, else none). */
|
|
39
|
-
export interface Block {
|
|
40
|
-
html: string;
|
|
41
|
-
text: string;
|
|
42
|
-
spacing: Spacing;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** An inline segment — for in-line emphasis inside a paragraph. */
|
|
46
|
-
export interface Inline {
|
|
47
|
-
html: string;
|
|
48
|
-
text: string;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Paragraph content: raw string(s) (auto-escaped for HTML) and/or `bold(...)`. */
|
|
52
|
-
export type InlineContent = string | Inline | Array<string | Inline>;
|
|
53
|
-
|
|
54
|
-
/** Inline emphasis segment. Emphasis is currently visually flat — it renders
|
|
55
|
-
* plain (no bold) in both surfaces; the seam is kept so all inline emphasis can
|
|
56
|
-
* be restyled in one place. */
|
|
57
|
-
export function bold(s: string): Inline {
|
|
58
|
-
return { html: escapeHtml(s), text: s };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Title-case a display value — capitalize the first letter of each word, e.g. a
|
|
62
|
-
* role like "admin" → "Admin". Leaves already-capitalized letters untouched. */
|
|
63
|
-
export function titleCase(s: string): string {
|
|
64
|
-
return s.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** Normalize a segment: a raw string is escaped for HTML, passed through for text. */
|
|
68
|
-
function toInline(seg: string | Inline): Inline {
|
|
69
|
-
return typeof seg === "string" ? { html: escapeHtml(seg), text: seg } : seg;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function renderInline(content: InlineContent): { html: string; text: string } {
|
|
73
|
-
const segs = (Array.isArray(content) ? content : [content]).map(toInline);
|
|
74
|
-
return {
|
|
75
|
-
html: segs.map((s) => s.html).join(""),
|
|
76
|
-
text: segs.map((s) => s.text).join(""),
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** Vertical spacing options for a paragraph. */
|
|
81
|
-
export type Spacing = "normal" | "tight" | "none";
|
|
82
|
-
const SPACING: Record<Spacing, string> = {
|
|
83
|
-
normal: "0 0 20px 0",
|
|
84
|
-
tight: "0 0 4px 0",
|
|
85
|
-
none: "0",
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
// =============================================================================
|
|
89
|
-
// Shells
|
|
90
|
-
// =============================================================================
|
|
91
|
-
|
|
92
|
-
/** Wrap body blocks in the shared `<!DOCTYPE>` monospace shell. */
|
|
93
|
-
export function htmlShell(blocks: Block[]): string {
|
|
94
|
-
const inner = blocks
|
|
95
|
-
.map((b) => b.html)
|
|
96
|
-
.filter(Boolean)
|
|
97
|
-
.join("\n");
|
|
98
|
-
return `<!DOCTYPE html>
|
|
99
|
-
<html lang="en">
|
|
100
|
-
<head><meta charset="UTF-8"></head>
|
|
101
|
-
<body style="${MONO} color: #1a1a1a; margin: 0; padding: 0;">
|
|
102
|
-
<div style="max-width: 520px; margin: 0 auto;">
|
|
103
|
-
${inner}
|
|
104
|
-
</div>
|
|
105
|
-
</body>
|
|
106
|
-
</html>`;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** Join body blocks into the plain-text email (blank-line separated per each
|
|
110
|
-
* block's spacing, trailing newline). */
|
|
111
|
-
export function textShell(blocks: Block[]): string {
|
|
112
|
-
let out = "";
|
|
113
|
-
blocks.forEach((b, i) => {
|
|
114
|
-
out += b.text;
|
|
115
|
-
if (i < blocks.length - 1) out += b.spacing === "normal" ? "\n\n" : "\n";
|
|
116
|
-
});
|
|
117
|
-
return out.trim() + "\n";
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// =============================================================================
|
|
121
|
-
// Paragraphs
|
|
122
|
-
// =============================================================================
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Email paragraph — mono (matching the body shell), 13px, configurable spacing.
|
|
126
|
-
* The single paragraph primitive: greeting / keyValue / footer / signature all
|
|
127
|
-
* build on it, and templates use it directly for body lines.
|
|
128
|
-
*/
|
|
129
|
-
export function paragraph(
|
|
130
|
-
content: InlineContent,
|
|
131
|
-
spacing: Spacing = "normal",
|
|
132
|
-
): Block {
|
|
133
|
-
const { html, text } = renderInline(content);
|
|
134
|
-
return {
|
|
135
|
-
html: `<p style="${MONO} font-size: ${FONT_SIZE}; margin: ${SPACING[spacing]};">${html}</p>`,
|
|
136
|
-
text,
|
|
137
|
-
spacing,
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/** "Hi Name," greeting; falls back to "Hi there," when the name is unknown. */
|
|
142
|
-
export function greeting(recipientName?: string): Block {
|
|
143
|
-
return paragraph(recipientName ? `Hi ${recipientName},` : "Hi there,");
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/** Security banner — the "WARNING" wordmark, sits atop security emails. */
|
|
147
|
-
export function security(): Block {
|
|
148
|
-
return paragraph("🆆🅰🆁🅽🅸🅽🅶");
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Format an ISO timestamp as "Jun 13, 2026". Pinned to en-US + UTC so the
|
|
153
|
-
* output is locale-/timezone-independent and render snapshots stay
|
|
154
|
-
* deterministic. Returns the raw input unchanged if it is not a parseable date.
|
|
155
|
-
*/
|
|
156
|
-
export function formatExpiry(iso: string): string {
|
|
157
|
-
const d = new Date(iso);
|
|
158
|
-
if (Number.isNaN(d.getTime())) return iso;
|
|
159
|
-
return d.toLocaleDateString("en-US", {
|
|
160
|
-
month: "short",
|
|
161
|
-
day: "numeric",
|
|
162
|
-
year: "numeric",
|
|
163
|
-
timeZone: "UTC",
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/** "Label: **value**" key/value row (value bold in HTML; tight by default). */
|
|
168
|
-
export function keyValue(
|
|
169
|
-
label: string,
|
|
170
|
-
value: string,
|
|
171
|
-
spacing: Spacing = "tight",
|
|
172
|
-
): Block {
|
|
173
|
-
return paragraph([`${label}: `, bold(value)], spacing);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/** Footer paragraph: first line + optional second line after a line break. */
|
|
177
|
-
export function footer(
|
|
178
|
-
firstLine: string,
|
|
179
|
-
secondLine?: string,
|
|
180
|
-
spacing: Spacing = "normal",
|
|
181
|
-
): Block {
|
|
182
|
-
const html = secondLine
|
|
183
|
-
? `${escapeHtml(firstLine)}<br>${escapeHtml(secondLine)}`
|
|
184
|
-
: escapeHtml(firstLine);
|
|
185
|
-
const text = secondLine ? `${firstLine}\n${secondLine}` : firstLine;
|
|
186
|
-
return {
|
|
187
|
-
html: `<p style="${MONO} font-size: ${FONT_SIZE}; margin: ${SPACING[spacing]};">${html}</p>`,
|
|
188
|
-
text,
|
|
189
|
-
spacing,
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Trailing sign-off — a leading blank line, the `---` rule, then the company
|
|
195
|
-
* (or custom `signer`) name. Owns the blank above the rule so every sign-off is
|
|
196
|
-
* spaced identically; the block before it carries no trailing gap.
|
|
197
|
-
*/
|
|
198
|
-
export function signature(signer: string = COMPANY_NAME): Block {
|
|
199
|
-
const line = `ⓒ ${new Date().getFullYear()} • ${signer}`;
|
|
200
|
-
const url = "https://companysemantics.ai";
|
|
201
|
-
return {
|
|
202
|
-
html: `<p style="${MONO} font-size: ${FONT_SIZE}; margin: ${SPACING.none};"><br><br><span style="color: #bbb;">/* EOM */</span><br>${escapeHtml(line)}<br><a href="${url}" target="_blank" rel="noopener noreferrer" style="color: #0047FF; text-decoration: none;">${escapeHtml(url)}</a></p>`,
|
|
203
|
-
text: `\n\n/* EOM */\n${line}\n${url}`,
|
|
204
|
-
spacing: "none",
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/** The standard "no action required" reassurance line (content constant). */
|
|
209
|
-
export const NOTICE = "If you weren't expecting this, no action is required.";
|
|
210
|
-
|
|
211
|
-
/** Human phrasing per access level (share-granted, access-approved). */
|
|
212
|
-
export const ACCESS_PHRASE: Record<"editor" | "commenter" | "viewer", string> =
|
|
213
|
-
{
|
|
214
|
-
editor: "can edit",
|
|
215
|
-
commenter: "can comment",
|
|
216
|
-
viewer: "can view",
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
// =============================================================================
|
|
220
|
-
// CTA box
|
|
221
|
-
// =============================================================================
|
|
222
|
-
|
|
223
|
-
/** Options for the `>> LABEL <<` call-to-action box. Styling is component-owned
|
|
224
|
-
* (the only allowable UI is the component) — callers supply only content. */
|
|
225
|
-
export interface CtaBoxOptions {
|
|
226
|
-
/** Text between the chevrons (already display-safe). */
|
|
227
|
-
label: string;
|
|
228
|
-
/** When present, the label links to this URL (and the URL rides under the
|
|
229
|
-
* plain-text box). */
|
|
230
|
-
href?: string;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/** Padding columns on each side of the `>> LABEL <<` line in the ASCII box. */
|
|
234
|
-
const CTA_BOX_PAD = 3;
|
|
235
|
-
|
|
236
|
-
/** `>> LABEL <<` ASCII box (`*` corners): 3 lines, sized to the label. */
|
|
237
|
-
function asciiCtaBox(label: string): string {
|
|
238
|
-
const pad = " ".repeat(CTA_BOX_PAD);
|
|
239
|
-
const inner = `${pad}>> ${label} <<${pad}`;
|
|
240
|
-
const border = `*${"-".repeat(inner.length)}*`;
|
|
241
|
-
return [border, `|${inner}|`, border].join("\n");
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/** The bordered `>> LABEL <<` button (HTML table + ascii text), with `margin` on
|
|
245
|
-
* the table. `ctaBox` wraps it as a standalone block; a chat unit embeds it
|
|
246
|
-
* (hence the export — internal to the render layer, not re-exported by `./index`). */
|
|
247
|
-
export function ctaButton(
|
|
248
|
-
opts: CtaBoxOptions,
|
|
249
|
-
margin: string,
|
|
250
|
-
): { html: string; text: string } {
|
|
251
|
-
const { label, href } = opts;
|
|
252
|
-
|
|
253
|
-
const tableStyle = `display: inline-block; border: 1px solid #666; border-radius: 2px; margin: ${margin}; max-width: 220px;`;
|
|
254
|
-
const tdStyle = `padding: 16px 24px; text-align: center; ${MONO} font-size: ${FONT_SIZE};`;
|
|
255
|
-
|
|
256
|
-
// Underline only the label text (not the chevrons/spaces), and only when linked.
|
|
257
|
-
const labelHtml = href
|
|
258
|
-
? `<span style="text-decoration: underline;">${escapeHtml(label)}</span>`
|
|
259
|
-
: escapeHtml(label);
|
|
260
|
-
const chevrons = `>> ${labelHtml} <<`;
|
|
261
|
-
const inner = href
|
|
262
|
-
? `<a href="${href}" style="color: #0047FF; text-decoration: none;">${chevrons}</a>`
|
|
263
|
-
: chevrons;
|
|
264
|
-
|
|
265
|
-
const html = `<table cellpadding="0" cellspacing="0" border="0" style="${tableStyle}">
|
|
266
|
-
<tr><td style="${tdStyle}">
|
|
267
|
-
${inner}
|
|
268
|
-
</td></tr>
|
|
269
|
-
</table>`;
|
|
270
|
-
|
|
271
|
-
const text = href ? `${asciiCtaBox(label)}\n\n${href}` : asciiCtaBox(label);
|
|
272
|
-
|
|
273
|
-
return { html, text };
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/** The bordered `>> LABEL <<` CTA box (OTP / JOIN / OPEN / VIEW TEAM / …). When
|
|
277
|
-
* `href` is given, HTML links the label and plain text prints the URL below. */
|
|
278
|
-
export function ctaBox(opts: CtaBoxOptions): Block {
|
|
279
|
-
const { html, text } = ctaButton(opts, "0 0 20px 0");
|
|
280
|
-
return { html, text, spacing: "normal" };
|
|
281
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Chat-shared email (someone shared a chat).
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { EmailPayloads } from "../types";
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
type Block,
|
|
9
|
-
footer,
|
|
10
|
-
greeting,
|
|
11
|
-
NOTICE,
|
|
12
|
-
paragraph,
|
|
13
|
-
signature,
|
|
14
|
-
} from "./blocks";
|
|
15
|
-
import { chatAssistant, chatCta, chatDots, chatUnit, chatUser } from "./chat";
|
|
16
|
-
import { COMPANY_NAME } from "./constants";
|
|
17
|
-
|
|
18
|
-
export type ChatSharedPayload = EmailPayloads["chat.shared"];
|
|
19
|
-
|
|
20
|
-
export function renderChatShared(payload: ChatSharedPayload): Block[] {
|
|
21
|
-
const { chatTitle, shareUrl, previewText, sharedByName } = payload;
|
|
22
|
-
|
|
23
|
-
return [
|
|
24
|
-
greeting(),
|
|
25
|
-
paragraph("A chat was shared with you."),
|
|
26
|
-
chatUnit(
|
|
27
|
-
chatUser(chatTitle, sharedByName),
|
|
28
|
-
...(previewText ? [chatAssistant(previewText)] : []),
|
|
29
|
-
chatDots(),
|
|
30
|
-
chatCta({ label: "SEE MORE", href: shareUrl }),
|
|
31
|
-
),
|
|
32
|
-
footer(`This share was sent via ${COMPANY_NAME}.`, NOTICE, "none"),
|
|
33
|
-
signature(),
|
|
34
|
-
];
|
|
35
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Company.md access-approved email (sent to the requester).
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { EmailPayloads } from "../types";
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
ACCESS_PHRASE,
|
|
9
|
-
type Block,
|
|
10
|
-
footer,
|
|
11
|
-
greeting,
|
|
12
|
-
keyValue,
|
|
13
|
-
paragraph,
|
|
14
|
-
signature,
|
|
15
|
-
} from "./blocks";
|
|
16
|
-
import { chatAssistant, chatCta, chatUnit } from "./chat";
|
|
17
|
-
import { COMPANY_NAME } from "./constants";
|
|
18
|
-
|
|
19
|
-
export type AccessApprovedPayload =
|
|
20
|
-
EmailPayloads["companyMd.access_request_approved"];
|
|
21
|
-
|
|
22
|
-
export function renderAccessApproved(payload: AccessApprovedPayload): Block[] {
|
|
23
|
-
const { approverName, docTitle, accessLevel, docUrl } = payload;
|
|
24
|
-
|
|
25
|
-
return [
|
|
26
|
-
greeting(),
|
|
27
|
-
paragraph("Your access request was approved."),
|
|
28
|
-
chatUnit(
|
|
29
|
-
chatAssistant("Open to view."),
|
|
30
|
-
chatCta({ label: "OPEN", href: docUrl }),
|
|
31
|
-
),
|
|
32
|
-
keyValue("Document", `"${docTitle}"`),
|
|
33
|
-
keyValue("Access", ACCESS_PHRASE[accessLevel]),
|
|
34
|
-
keyValue("Approved by", approverName, "normal"),
|
|
35
|
-
footer(
|
|
36
|
-
`This notification was sent via ${COMPANY_NAME}.`,
|
|
37
|
-
"Access is now active.",
|
|
38
|
-
"none",
|
|
39
|
-
),
|
|
40
|
-
signature(),
|
|
41
|
-
];
|
|
42
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Company.md access-denied email (sent to the requester). No CTA.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { EmailPayloads } from "../types";
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
type Block,
|
|
9
|
-
footer,
|
|
10
|
-
greeting,
|
|
11
|
-
keyValue,
|
|
12
|
-
paragraph,
|
|
13
|
-
signature,
|
|
14
|
-
} from "./blocks";
|
|
15
|
-
import { chatUnit, chatUser } from "./chat";
|
|
16
|
-
import { COMPANY_NAME } from "./constants";
|
|
17
|
-
|
|
18
|
-
export type AccessDeniedPayload =
|
|
19
|
-
EmailPayloads["companyMd.access_request_denied"];
|
|
20
|
-
|
|
21
|
-
export function renderAccessDenied(payload: AccessDeniedPayload): Block[] {
|
|
22
|
-
const { approverName, docTitle, reason } = payload;
|
|
23
|
-
|
|
24
|
-
const blocks: Block[] = [
|
|
25
|
-
greeting(),
|
|
26
|
-
paragraph("Your access request was declined."),
|
|
27
|
-
];
|
|
28
|
-
|
|
29
|
-
if (reason) blocks.push(chatUnit(chatUser(reason, approverName)));
|
|
30
|
-
|
|
31
|
-
blocks.push(
|
|
32
|
-
keyValue("Document", `"${docTitle}"`),
|
|
33
|
-
keyValue("Declined by", approverName, "normal"),
|
|
34
|
-
footer(
|
|
35
|
-
`This notification was sent via ${COMPANY_NAME}.`,
|
|
36
|
-
"This request is now closed.",
|
|
37
|
-
"none",
|
|
38
|
-
),
|
|
39
|
-
signature(),
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
return blocks;
|
|
43
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Company.md access-requested email (sent to the doc owner).
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { EmailPayloads } from "../types";
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
type Block,
|
|
9
|
-
footer,
|
|
10
|
-
greeting,
|
|
11
|
-
keyValue,
|
|
12
|
-
paragraph,
|
|
13
|
-
signature,
|
|
14
|
-
} from "./blocks";
|
|
15
|
-
import { chatAssistant, chatCta, chatUnit, chatUser } from "./chat";
|
|
16
|
-
import { COMPANY_NAME } from "./constants";
|
|
17
|
-
|
|
18
|
-
export type AccessRequestedPayload =
|
|
19
|
-
EmailPayloads["companyMd.access_requested"];
|
|
20
|
-
|
|
21
|
-
const OWNER_NOTE = "You own this document.";
|
|
22
|
-
|
|
23
|
-
export function renderAccessRequested(
|
|
24
|
-
payload: AccessRequestedPayload,
|
|
25
|
-
): Block[] {
|
|
26
|
-
const { requesterName, docTitle, message, reviewUrl } = payload;
|
|
27
|
-
|
|
28
|
-
return [
|
|
29
|
-
greeting(),
|
|
30
|
-
paragraph("Someone requested document access."),
|
|
31
|
-
chatUnit(
|
|
32
|
-
message
|
|
33
|
-
? chatUser(message, requesterName)
|
|
34
|
-
: chatAssistant("Review the request."),
|
|
35
|
-
chatCta({ label: "REVIEW", href: reviewUrl }),
|
|
36
|
-
),
|
|
37
|
-
keyValue("From", requesterName),
|
|
38
|
-
keyValue("Document", `"${docTitle}"`, "normal"),
|
|
39
|
-
footer(
|
|
40
|
-
`This notification was sent via ${COMPANY_NAME}.`,
|
|
41
|
-
OWNER_NOTE,
|
|
42
|
-
"none",
|
|
43
|
-
),
|
|
44
|
-
signature(),
|
|
45
|
-
];
|
|
46
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Email render constants.
|
|
3
|
-
*
|
|
4
|
-
* Branding + layout constants shared by every email template. These are the
|
|
5
|
-
* single source of truth for both the backend (real sent emails) and the app
|
|
6
|
-
* (Ladle preview). `EMAIL_FROM` (SES envelope) is infra and stays in the
|
|
7
|
-
* backend, not here.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/** Product name shown in footers and signatures. */
|
|
11
|
-
export const COMPANY_NAME = "Company Semantics";
|
|
12
|
-
|
|
13
|
-
/** Support address shown in the plain-text footer. */
|
|
14
|
-
export const SUPPORT_EMAIL = "support@companysemantics.ai";
|
|
15
|
-
|
|
16
|
-
/** Monospace font stack for HTML emails. */
|
|
17
|
-
export const MONO_FONT_STACK =
|
|
18
|
-
"'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace";
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Email render layer — the single source of truth for transactional email
|
|
3
|
-
* markup, composed by both the backend (real sends) and the app (Ladle preview).
|
|
4
|
-
*
|
|
5
|
-
* Import from `@company-semantics/contracts/email`.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
// Constants
|
|
9
|
-
export { COMPANY_NAME, SUPPORT_EMAIL, MONO_FONT_STACK } from "./constants";
|
|
10
|
-
|
|
11
|
-
// Primitives
|
|
12
|
-
export { escapeHtml } from "./escape-html";
|
|
13
|
-
|
|
14
|
-
// Dual-output building blocks (each returns a `Block` = { html, text })
|
|
15
|
-
export {
|
|
16
|
-
htmlShell,
|
|
17
|
-
textShell,
|
|
18
|
-
paragraph,
|
|
19
|
-
greeting,
|
|
20
|
-
security,
|
|
21
|
-
keyValue,
|
|
22
|
-
footer,
|
|
23
|
-
signature,
|
|
24
|
-
NOTICE,
|
|
25
|
-
ACCESS_PHRASE,
|
|
26
|
-
ctaBox,
|
|
27
|
-
bold,
|
|
28
|
-
type Block,
|
|
29
|
-
type Inline,
|
|
30
|
-
type InlineContent,
|
|
31
|
-
type Spacing,
|
|
32
|
-
type CtaBoxOptions,
|
|
33
|
-
} from "./blocks";
|
|
34
|
-
|
|
35
|
-
// The chat unit — same `Block` contract, its own module (bubble geometry)
|
|
36
|
-
export {
|
|
37
|
-
chatDots,
|
|
38
|
-
chatUser,
|
|
39
|
-
chatAssistant,
|
|
40
|
-
chatCta,
|
|
41
|
-
chatUnit,
|
|
42
|
-
type ChatMessage,
|
|
43
|
-
type ChatCta,
|
|
44
|
-
type ChatItem,
|
|
45
|
-
} from "./chat";
|
|
46
|
-
|
|
47
|
-
// Dispatcher
|
|
48
|
-
export {
|
|
49
|
-
renderEmail,
|
|
50
|
-
IMPLEMENTED_EMAIL_KINDS,
|
|
51
|
-
type ImplementedEmailKind,
|
|
52
|
-
type RenderedEmail,
|
|
53
|
-
type RenderEmailOptions,
|
|
54
|
-
} from "./render-email";
|
|
55
|
-
|
|
56
|
-
// Template payload types + alert vocabulary
|
|
57
|
-
export type { AuthOtpPayload, RenderOptions } from "./auth-otp";
|
|
58
|
-
export { SECURITY_ALERT_TYPES, type SecurityAlertType } from "./security-alert";
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Org invite email (join a workspace).
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { EmailPayloads } from "../types";
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
type Block,
|
|
9
|
-
footer,
|
|
10
|
-
formatExpiry,
|
|
11
|
-
greeting,
|
|
12
|
-
keyValue,
|
|
13
|
-
NOTICE,
|
|
14
|
-
paragraph,
|
|
15
|
-
signature,
|
|
16
|
-
titleCase,
|
|
17
|
-
} from "./blocks";
|
|
18
|
-
import { chatAssistant, chatCta, chatUnit } from "./chat";
|
|
19
|
-
import { COMPANY_NAME } from "./constants";
|
|
20
|
-
|
|
21
|
-
export type OrgInvitePayload = EmailPayloads["org.invite"];
|
|
22
|
-
|
|
23
|
-
export function renderOrgInvite(payload: OrgInvitePayload): Block[] {
|
|
24
|
-
const { inviterName, orgName, role, acceptUrl, expiresAt } = payload;
|
|
25
|
-
|
|
26
|
-
return [
|
|
27
|
-
greeting(),
|
|
28
|
-
paragraph(`${orgName} uses Company Semantics.`),
|
|
29
|
-
chatUnit(
|
|
30
|
-
chatAssistant("Join to accept the invitation."),
|
|
31
|
-
chatCta({ label: "JOIN", href: acceptUrl }),
|
|
32
|
-
),
|
|
33
|
-
keyValue("From", inviterName),
|
|
34
|
-
keyValue("Workspace", orgName),
|
|
35
|
-
keyValue("Role", titleCase(role)),
|
|
36
|
-
keyValue("Expires", formatExpiry(expiresAt), "normal"),
|
|
37
|
-
footer(`This invitation was sent via ${COMPANY_NAME}.`, NOTICE, "none"),
|
|
38
|
-
signature(),
|
|
39
|
-
];
|
|
40
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ownership-transfer-completed notice (plain text only).
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { EmailPayloads } from "../types";
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
type Block,
|
|
9
|
-
footer,
|
|
10
|
-
greeting,
|
|
11
|
-
keyValue,
|
|
12
|
-
paragraph,
|
|
13
|
-
signature,
|
|
14
|
-
} from "./blocks";
|
|
15
|
-
import { chatAssistant, chatUnit } from "./chat";
|
|
16
|
-
import { COMPANY_NAME } from "./constants";
|
|
17
|
-
|
|
18
|
-
export type OwnershipTransferCompletedPayload =
|
|
19
|
-
EmailPayloads["org.ownership_transfer_completed"];
|
|
20
|
-
|
|
21
|
-
export function renderOwnershipTransferCompleted(
|
|
22
|
-
payload: OwnershipTransferCompletedPayload,
|
|
23
|
-
): Block[] {
|
|
24
|
-
const { orgName, newOwnerEmail } = payload;
|
|
25
|
-
|
|
26
|
-
return [
|
|
27
|
-
greeting(),
|
|
28
|
-
paragraph("Ownership was transferred."),
|
|
29
|
-
chatUnit(
|
|
30
|
-
chatAssistant("You're no longer the owner, but you remain an admin."),
|
|
31
|
-
),
|
|
32
|
-
keyValue("Workspace", orgName),
|
|
33
|
-
keyValue("New owner", newOwnerEmail, "normal"),
|
|
34
|
-
footer(
|
|
35
|
-
`This confirmation was sent via ${COMPANY_NAME}.`,
|
|
36
|
-
"If you did not authorize this, contact support immediately.",
|
|
37
|
-
"none",
|
|
38
|
-
),
|
|
39
|
-
signature(),
|
|
40
|
-
];
|
|
41
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ownership-transfer invitation email (plain text only).
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { EmailPayloads } from "../types";
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
type Block,
|
|
9
|
-
footer,
|
|
10
|
-
formatExpiry,
|
|
11
|
-
greeting,
|
|
12
|
-
keyValue,
|
|
13
|
-
NOTICE,
|
|
14
|
-
paragraph,
|
|
15
|
-
signature,
|
|
16
|
-
} from "./blocks";
|
|
17
|
-
import { chatAssistant, chatCta, chatUnit, chatUser } from "./chat";
|
|
18
|
-
import { COMPANY_NAME } from "./constants";
|
|
19
|
-
|
|
20
|
-
export type OwnershipTransferPayload = EmailPayloads["org.ownership_transfer"];
|
|
21
|
-
|
|
22
|
-
export function renderOwnershipTransfer(
|
|
23
|
-
payload: OwnershipTransferPayload,
|
|
24
|
-
): Block[] {
|
|
25
|
-
const { orgName, acceptUrl, expiresAt, note, fromName } = payload;
|
|
26
|
-
|
|
27
|
-
return [
|
|
28
|
-
greeting(),
|
|
29
|
-
paragraph("You've been invited to take ownership."),
|
|
30
|
-
chatUnit(
|
|
31
|
-
note
|
|
32
|
-
? chatUser(note, fromName)
|
|
33
|
-
: chatAssistant("Accept to take ownership."),
|
|
34
|
-
chatCta({ label: "ACCEPT", href: acceptUrl }),
|
|
35
|
-
),
|
|
36
|
-
...(fromName ? [keyValue("From", fromName)] : []),
|
|
37
|
-
keyValue("Workspace", orgName),
|
|
38
|
-
keyValue("Expires", formatExpiry(expiresAt), "normal"),
|
|
39
|
-
footer(`This transfer was sent via ${COMPANY_NAME}.`, NOTICE, "none"),
|
|
40
|
-
signature(),
|
|
41
|
-
];
|
|
42
|
-
}
|