@company-semantics/contracts 27.13.0 → 27.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/email/render/__tests__/__snapshots__/render-snapshot.test.ts.snap +155 -146
- package/src/email/render/__tests__/render-snapshot.test.ts +3 -1
- package/src/email/render/auth-otp.ts +8 -9
- package/src/email/render/blocks.ts +115 -97
- package/src/email/render/chat-shared.ts +8 -23
- package/src/email/render/company-md-access-approved.ts +2 -2
- package/src/email/render/company-md-access-denied.ts +6 -11
- package/src/email/render/company-md-access-requested.ts +8 -13
- package/src/email/render/constants.ts +0 -6
- package/src/email/render/index.ts +6 -13
- package/src/email/render/org-invite.ts +11 -11
- package/src/email/render/ownership-transfer.ts +2 -2
- package/src/email/render/render-email.ts +13 -5
- package/src/email/render/share-granted.ts +8 -13
- package/src/email/render/unit-owner-granted.ts +8 -13
|
@@ -28,10 +28,12 @@ function add<K extends ImplementedEmailKind>(
|
|
|
28
28
|
payload: EmailPayloads[K],
|
|
29
29
|
options?: RenderEmailOptions,
|
|
30
30
|
): void {
|
|
31
|
+
// `payload as never`: the add() call sites are type-checked (payload:
|
|
32
|
+
// EmailPayloads[K]); this only bridges the generic conditional in renderEmail.
|
|
31
33
|
fixtures.push({
|
|
32
34
|
kind,
|
|
33
35
|
name,
|
|
34
|
-
render: () => renderEmail(kind, payload, options),
|
|
36
|
+
render: () => renderEmail(kind, payload as never, options),
|
|
35
37
|
});
|
|
36
38
|
}
|
|
37
39
|
|
|
@@ -11,13 +11,12 @@ import type { EmailPayloads } from "../types";
|
|
|
11
11
|
import { baseTextLayout } from "./base-text";
|
|
12
12
|
import {
|
|
13
13
|
asciiCodeBox,
|
|
14
|
-
|
|
14
|
+
paragraph,
|
|
15
15
|
ctaBox,
|
|
16
16
|
footer,
|
|
17
17
|
htmlShell,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
signatureLine,
|
|
18
|
+
keyValue,
|
|
19
|
+
signature,
|
|
21
20
|
} from "./blocks";
|
|
22
21
|
import { COMPANY_NAME } from "./constants";
|
|
23
22
|
|
|
@@ -67,7 +66,7 @@ export function renderAuthOtpHtml(payload: AuthOtpPayload): string {
|
|
|
67
66
|
const { otp, expiresInMinutes } = payload;
|
|
68
67
|
|
|
69
68
|
return htmlShell([
|
|
70
|
-
|
|
69
|
+
paragraph("Copy/paste code in login form:"),
|
|
71
70
|
ctaBox({
|
|
72
71
|
label: otp,
|
|
73
72
|
padding: "20px 24px",
|
|
@@ -75,8 +74,8 @@ export function renderAuthOtpHtml(payload: AuthOtpPayload): string {
|
|
|
75
74
|
borderRadiusZero: true,
|
|
76
75
|
letterSpacing: "4px",
|
|
77
76
|
}),
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
keyValue("Status", "VALID", "tight"),
|
|
78
|
+
keyValue(
|
|
80
79
|
"Expires in",
|
|
81
80
|
`${expiresInMinutes} ${expiresInMinutes === 1 ? "min" : "mins"}`,
|
|
82
81
|
"normal",
|
|
@@ -85,7 +84,7 @@ export function renderAuthOtpHtml(payload: AuthOtpPayload): string {
|
|
|
85
84
|
`This code was generated to authorize a login to ${COMPANY_NAME}.`,
|
|
86
85
|
"It expires automatically and cannot be reused.",
|
|
87
86
|
),
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
paragraph("If this wasn't you, no action is required."),
|
|
88
|
+
signature(),
|
|
90
89
|
]);
|
|
91
90
|
}
|
|
@@ -11,11 +11,7 @@
|
|
|
11
11
|
* - Callers pass already-escaped content for any user-controlled HTML field.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
15
|
-
COMPANY_NAME,
|
|
16
|
-
MONO_FONT_STACK,
|
|
17
|
-
TITLE_TRUNCATE_LENGTH,
|
|
18
|
-
} from "./constants";
|
|
14
|
+
import { COMPANY_NAME, MONO_FONT_STACK } from "./constants";
|
|
19
15
|
import { escapeHtml } from "./escape-html";
|
|
20
16
|
|
|
21
17
|
const MONO = `font-family: ${MONO_FONT_STACK};`;
|
|
@@ -50,33 +46,27 @@ const SPACING: Record<Spacing, string> = {
|
|
|
50
46
|
none: "0",
|
|
51
47
|
};
|
|
52
48
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Email paragraph — mono (matching the body shell), 14px, with configurable
|
|
51
|
+
* vertical spacing. The single paragraph primitive: greeting / meta / footer /
|
|
52
|
+
* signature all build on it, and templates use it directly for body lines.
|
|
53
|
+
*/
|
|
54
|
+
export function paragraph(html: string, spacing: Spacing = "normal"): string {
|
|
55
|
+
return `<p style="${MONO} font-size: 14px; margin: ${SPACING[spacing]};">${html}</p>`;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/** "Hi Name," / "Hi," greeting. Escapes the name. */
|
|
59
59
|
export function greeting(recipientName?: string): string {
|
|
60
|
-
return
|
|
61
|
-
recipientName ? `Hi ${escapeHtml(recipientName)},` : "Hi,",
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Monospace paragraph for meta / footer / notice lines. */
|
|
66
|
-
export function monoParagraph(
|
|
67
|
-
html: string,
|
|
68
|
-
spacing: Spacing = "normal",
|
|
69
|
-
): string {
|
|
70
|
-
return `<p style="${MONO} font-size: 14px; margin: ${SPACING[spacing]};">${html}</p>`;
|
|
60
|
+
return paragraph(recipientName ? `Hi ${escapeHtml(recipientName)},` : "Hi,");
|
|
71
61
|
}
|
|
72
62
|
|
|
73
|
-
/** "Label: <b>value</b>"
|
|
74
|
-
export function
|
|
63
|
+
/** "Label: <b>value</b>" key/value row. */
|
|
64
|
+
export function keyValue(
|
|
75
65
|
label: string,
|
|
76
66
|
valueHtml: string,
|
|
77
67
|
spacing: Spacing = "tight",
|
|
78
68
|
): string {
|
|
79
|
-
return
|
|
69
|
+
return paragraph(`${label}: <b>${valueHtml}</b>`, spacing);
|
|
80
70
|
}
|
|
81
71
|
|
|
82
72
|
/** Footer paragraph: first line + optional second line after a `<br>`. */
|
|
@@ -85,15 +75,15 @@ export function footer(
|
|
|
85
75
|
secondLine?: string,
|
|
86
76
|
spacing: Spacing = "normal",
|
|
87
77
|
): string {
|
|
88
|
-
return
|
|
78
|
+
return paragraph(
|
|
89
79
|
secondLine ? `${firstLine}<br>${secondLine}` : firstLine,
|
|
90
80
|
spacing,
|
|
91
81
|
);
|
|
92
82
|
}
|
|
93
83
|
|
|
94
84
|
/** Trailing company signature paragraph. */
|
|
95
|
-
export function
|
|
96
|
-
return
|
|
85
|
+
export function signature(): string {
|
|
86
|
+
return paragraph(COMPANY_NAME, "none");
|
|
97
87
|
}
|
|
98
88
|
|
|
99
89
|
/** The standard "no action required" reassurance line (content constant). */
|
|
@@ -168,38 +158,39 @@ export function ctaBox(opts: CtaBoxOptions): string {
|
|
|
168
158
|
</table>`;
|
|
169
159
|
}
|
|
170
160
|
|
|
171
|
-
// =============================================================================
|
|
172
|
-
// HTML — quote block
|
|
173
|
-
// =============================================================================
|
|
174
|
-
|
|
175
|
-
/** The left-ruled quoted-message block. Both args must be pre-escaped. */
|
|
176
|
-
export function quoteBlock(labelHtml: string, bodyHtml: string): string {
|
|
177
|
-
return `<table cellpadding="0" cellspacing="0" border="0" style="margin: 0 0 20px 0;">
|
|
178
|
-
<tr><td style="border-left: 2px solid #1a1a1a; padding: 4px 14px; ${MONO} font-size: 13px; background: #f5f5f5;">
|
|
179
|
-
<span style="color: #555;">${labelHtml}</span><br>${bodyHtml}
|
|
180
|
-
</td></tr>
|
|
181
|
-
</table>`;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
161
|
// =============================================================================
|
|
185
162
|
// HTML — chat bubbles
|
|
186
163
|
// =============================================================================
|
|
187
164
|
|
|
188
|
-
/**
|
|
189
|
-
|
|
165
|
+
/**
|
|
166
|
+
* Right-aligned user chat bubble. Pass raw text — it is truncated by
|
|
167
|
+
* clampMessage and escaped here. When `from` is given (raw) it renders as an
|
|
168
|
+
* attribution line below the bubble, right-aligned to the bubble's right edge
|
|
169
|
+
* (clear of the face). Doubles as the "message from …" quote.
|
|
170
|
+
*/
|
|
171
|
+
export function chatUserBubble(text: string, from?: string): string {
|
|
172
|
+
const contentHtml = escapeHtml(clampMessage(text));
|
|
173
|
+
const attributionRow = from
|
|
174
|
+
? `
|
|
175
|
+
<tr>
|
|
176
|
+
<td style="${MONO} font-size: 11px; color: #888; text-align: right; padding-top: 4px;">${escapeHtml(from)}</td>
|
|
177
|
+
<td></td>
|
|
178
|
+
</tr>`
|
|
179
|
+
: "";
|
|
190
180
|
return `<table cellpadding="0" cellspacing="0" border="0" style="margin: 0 0 16px 0;">
|
|
191
181
|
<tr>
|
|
192
182
|
<td style="border: 1px solid #1a1a1a; border-radius: 12px 12px 0 12px; padding: 10px 14px; min-width: 280px; text-align: right; ${MONO} font-size: 13px; background: #f5f5f5;">${contentHtml}</td>
|
|
193
183
|
<td style="${MONO} font-size: 14px; padding-left: 8px; vertical-align: bottom;">(•̀_ರ╮)</td>
|
|
194
|
-
</tr
|
|
184
|
+
</tr>${attributionRow}
|
|
195
185
|
</table>`;
|
|
196
186
|
}
|
|
197
187
|
|
|
198
|
-
/** Left-aligned assistant chat bubble.
|
|
199
|
-
export function chatAssistantBubble(
|
|
188
|
+
/** Left-aligned assistant chat bubble. Pass raw text — truncated + escaped here. */
|
|
189
|
+
export function chatAssistantBubble(text: string): string {
|
|
190
|
+
const contentHtml = escapeHtml(clampMessage(text));
|
|
200
191
|
return `<table cellpadding="0" cellspacing="0" border="0" style="margin: 16px 0;">
|
|
201
192
|
<tr>
|
|
202
|
-
<td style="${MONO} font-size: 14px; padding-right: 8px; vertical-align:
|
|
193
|
+
<td style="${MONO} font-size: 14px; padding-right: 8px; vertical-align: bottom;">[c_S]</td>
|
|
203
194
|
<td style="border: 1px solid #1a1a1a; border-radius: 12px 12px 12px 0; padding: 10px 14px; min-width: 280px; ${MONO} font-size: 13px; background: #f5f5f5;">${contentHtml}</td>
|
|
204
195
|
</tr>
|
|
205
196
|
</table>`;
|
|
@@ -209,9 +200,11 @@ export function chatAssistantBubble(contentHtml: string): string {
|
|
|
209
200
|
// Plain text — CTA / code boxes
|
|
210
201
|
// =============================================================================
|
|
211
202
|
|
|
212
|
-
const CTA_BOX_WIDTH = 20;
|
|
213
203
|
const CODE_BOX_WIDTH = 18;
|
|
214
204
|
|
|
205
|
+
/** Padding columns on each side of the `>> LABEL <<` line in the CTA box. */
|
|
206
|
+
const CTA_BOX_PAD = 3;
|
|
207
|
+
|
|
215
208
|
function centered(value: string, width: number): string {
|
|
216
209
|
const pad = Math.max(0, width - value.length);
|
|
217
210
|
const left = Math.floor(pad / 2);
|
|
@@ -219,18 +212,15 @@ function centered(value: string, width: number): string {
|
|
|
219
212
|
return `${" ".repeat(left)}${value}${" ".repeat(right)}`;
|
|
220
213
|
}
|
|
221
214
|
|
|
222
|
-
/**
|
|
215
|
+
/**
|
|
216
|
+
* `>> LABEL <<` ASCII box (`*` corners): 3 lines, sized to the label with
|
|
217
|
+
* `CTA_BOX_PAD` padding columns on each side.
|
|
218
|
+
*/
|
|
223
219
|
export function asciiCtaBox(label: string): string {
|
|
224
|
-
const
|
|
225
|
-
const
|
|
226
|
-
const
|
|
227
|
-
return [
|
|
228
|
-
border,
|
|
229
|
-
blank,
|
|
230
|
-
`|${centered(line, CTA_BOX_WIDTH)}|`,
|
|
231
|
-
blank,
|
|
232
|
-
border,
|
|
233
|
-
].join("\n");
|
|
220
|
+
const pad = " ".repeat(CTA_BOX_PAD);
|
|
221
|
+
const inner = `${pad}>> ${label} <<${pad}`;
|
|
222
|
+
const border = `*${"-".repeat(inner.length)}*`;
|
|
223
|
+
return [border, `|${inner}|`, border].join("\n");
|
|
234
224
|
}
|
|
235
225
|
|
|
236
226
|
/** ASCII box that displays a code value (OTP; `+` corners, centered). */
|
|
@@ -247,12 +237,48 @@ export function asciiCodeBox(value: string): string {
|
|
|
247
237
|
}
|
|
248
238
|
|
|
249
239
|
// =============================================================================
|
|
250
|
-
// Plain text —
|
|
240
|
+
// Plain text — chat boxes
|
|
251
241
|
// =============================================================================
|
|
252
242
|
|
|
253
|
-
/**
|
|
254
|
-
|
|
255
|
-
|
|
243
|
+
/** Greedy word-wrap into lines of at most `width` chars (hard-breaks long words). */
|
|
244
|
+
function wrapText(text: string, width: number): string[] {
|
|
245
|
+
const lines: string[] = [];
|
|
246
|
+
let cur = "";
|
|
247
|
+
for (const word of text.split(/\s+/).filter(Boolean)) {
|
|
248
|
+
let w = word;
|
|
249
|
+
while (w.length > width) {
|
|
250
|
+
if (cur) {
|
|
251
|
+
lines.push(cur);
|
|
252
|
+
cur = "";
|
|
253
|
+
}
|
|
254
|
+
lines.push(w.slice(0, width));
|
|
255
|
+
w = w.slice(width);
|
|
256
|
+
}
|
|
257
|
+
if (!cur) cur = w;
|
|
258
|
+
else if (cur.length + 1 + w.length <= width) cur += ` ${w}`;
|
|
259
|
+
else {
|
|
260
|
+
lines.push(cur);
|
|
261
|
+
cur = w;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (cur) lines.push(cur);
|
|
265
|
+
return lines.length ? lines : [""];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Chat message truncation budget: at most MAX_MESSAGE_LINES lines of MESSAGE_WIDTH chars. */
|
|
269
|
+
const MAX_MESSAGE_LINES = 3;
|
|
270
|
+
const MESSAGE_WIDTH = 35;
|
|
271
|
+
|
|
272
|
+
/** Word-wrap `text`, then clamp to `maxLines`, ellipsizing the last line on overflow. */
|
|
273
|
+
function wrapClamped(text: string, width: number, maxLines: number): string[] {
|
|
274
|
+
const lines = wrapText(text, width);
|
|
275
|
+
if (lines.length <= maxLines) return lines;
|
|
276
|
+
const kept = lines.slice(0, maxLines);
|
|
277
|
+
const last = kept[maxLines - 1];
|
|
278
|
+
kept[maxLines - 1] =
|
|
279
|
+
(last.length > width - 3 ? last.slice(0, width - 3).trimEnd() : last) +
|
|
280
|
+
"...";
|
|
281
|
+
return kept;
|
|
256
282
|
}
|
|
257
283
|
|
|
258
284
|
/** Plain-text continuation dots, centered under the 38-char chat box. */
|
|
@@ -260,49 +286,41 @@ export function chatDotsText(): string {
|
|
|
260
286
|
return " ⋮";
|
|
261
287
|
}
|
|
262
288
|
|
|
263
|
-
/**
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
return
|
|
271
|
-
|
|
272
|
-
|
|
289
|
+
/**
|
|
290
|
+
* The one truncation authority: clamp a raw message to MAX_MESSAGE_LINES ×
|
|
291
|
+
* MESSAGE_WIDTH, ellipsized. Every chat box/bubble runs its content through
|
|
292
|
+
* this, so HTML and plain text truncate at exactly the same point. Pass raw
|
|
293
|
+
* (unescaped) text.
|
|
294
|
+
*/
|
|
295
|
+
export function clampMessage(text: string): string {
|
|
296
|
+
return wrapClamped(text, MESSAGE_WIDTH, MAX_MESSAGE_LINES).join(" ");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Right-aligned user chat box. `content` is truncated by clampMessage, then
|
|
301
|
+
* wrapped for display. The kaomoji rides the bottom border — bottom-aligned
|
|
302
|
+
* like a chat avatar. When `from` is given it renders as an attribution line
|
|
303
|
+
* below the box, right-aligned to the box's right edge (clear of the face).
|
|
304
|
+
* Doubles as the "message from …" quote.
|
|
305
|
+
*/
|
|
306
|
+
export function chatUserBoxText(content: string, from?: string): string {
|
|
307
|
+
const border = "─".repeat(MESSAGE_WIDTH + 1);
|
|
308
|
+
const lines = wrapText(clampMessage(content), MESSAGE_WIDTH);
|
|
309
|
+
const body = lines.map((line) => `│${line.padStart(MESSAGE_WIDTH)} │`);
|
|
310
|
+
const box = [`┌${border}┐`, ...body, `└${border}┘ (•̀_ರ╮)`];
|
|
311
|
+
if (from) box.push(from.padStart(MESSAGE_WIDTH + 3));
|
|
312
|
+
return box.join("\n");
|
|
273
313
|
}
|
|
274
314
|
|
|
275
|
-
/** Left-aligned assistant chat box with `[c_S]` label (
|
|
315
|
+
/** Left-aligned assistant chat box with `[c_S]` label (clampMessage-truncated, 36 wide). */
|
|
276
316
|
export function chatAssistantBoxText(text: string): string {
|
|
277
317
|
const lineWidth = 36;
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
let truncated = text;
|
|
281
|
-
if (text.length > maxChars - 3) {
|
|
282
|
-
truncated = text.slice(0, maxChars - 3) + "...";
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const lines: string[] = [];
|
|
286
|
-
let remaining = truncated;
|
|
287
|
-
while (remaining.length > 0 && lines.length < 2) {
|
|
288
|
-
if (remaining.length <= lineWidth) {
|
|
289
|
-
lines.push(remaining);
|
|
290
|
-
remaining = "";
|
|
291
|
-
} else {
|
|
292
|
-
let breakPoint = remaining.lastIndexOf(" ", lineWidth);
|
|
293
|
-
if (breakPoint <= 0) breakPoint = lineWidth;
|
|
294
|
-
lines.push(remaining.slice(0, breakPoint));
|
|
295
|
-
remaining = remaining.slice(breakPoint).trimStart();
|
|
296
|
-
}
|
|
297
|
-
}
|
|
318
|
+
const lines = wrapText(clampMessage(text), lineWidth);
|
|
298
319
|
|
|
299
320
|
const top = " ┌──────────────────────────────────────┐";
|
|
300
|
-
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
const label = i === 0 ? "[c_S] " : " ";
|
|
304
|
-
return `${label}│${padded}│`;
|
|
305
|
-
});
|
|
321
|
+
// [c_S] rides the bottom border — bottom-aligned like a chat avatar.
|
|
322
|
+
const bot = "[c_S] └──────────────────────────────────────┘";
|
|
323
|
+
const boxLines = lines.map((line) => ` │ ${line.padEnd(lineWidth)} │`);
|
|
306
324
|
|
|
307
325
|
return [top, ...boxLines, bot].join("\n");
|
|
308
326
|
}
|
|
@@ -7,7 +7,7 @@ import type { EmailPayloads } from "../types";
|
|
|
7
7
|
import { baseTextLayout } from "./base-text";
|
|
8
8
|
import {
|
|
9
9
|
asciiCtaBox,
|
|
10
|
-
|
|
10
|
+
paragraph,
|
|
11
11
|
chatAssistantBoxText,
|
|
12
12
|
chatAssistantBubble,
|
|
13
13
|
chatDots,
|
|
@@ -17,23 +17,13 @@ import {
|
|
|
17
17
|
ctaBox,
|
|
18
18
|
footer,
|
|
19
19
|
htmlShell,
|
|
20
|
-
monoParagraph,
|
|
21
20
|
NOTICE,
|
|
22
|
-
|
|
21
|
+
signature,
|
|
23
22
|
} from "./blocks";
|
|
24
|
-
import {
|
|
25
|
-
COMPANY_NAME,
|
|
26
|
-
PREVIEW_TRUNCATE_LENGTH,
|
|
27
|
-
TITLE_TRUNCATE_LENGTH,
|
|
28
|
-
} from "./constants";
|
|
29
|
-
import { escapeHtml } from "./escape-html";
|
|
23
|
+
import { COMPANY_NAME } from "./constants";
|
|
30
24
|
|
|
31
25
|
export type ChatSharedPayload = EmailPayloads["chat.shared"];
|
|
32
26
|
|
|
33
|
-
function truncate(text: string, max: number): string {
|
|
34
|
-
return text.length > max ? text.slice(0, max - 3) + "..." : text;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
27
|
export function renderChatSharedBody(payload: ChatSharedPayload): string {
|
|
38
28
|
const { chatTitle, shareUrl, previewText } = payload;
|
|
39
29
|
|
|
@@ -64,16 +54,11 @@ export function renderChatSharedBody(payload: ChatSharedPayload): string {
|
|
|
64
54
|
export function renderChatSharedHtml(payload: ChatSharedPayload): string {
|
|
65
55
|
const { chatTitle, shareUrl, previewText } = payload;
|
|
66
56
|
|
|
67
|
-
const
|
|
68
|
-
const previewHtml = previewText
|
|
69
|
-
? chatAssistantBubble(
|
|
70
|
-
escapeHtml(truncate(previewText, PREVIEW_TRUNCATE_LENGTH)),
|
|
71
|
-
)
|
|
72
|
-
: "";
|
|
57
|
+
const previewHtml = previewText ? chatAssistantBubble(previewText) : "";
|
|
73
58
|
|
|
74
59
|
return htmlShell([
|
|
75
|
-
|
|
76
|
-
chatUserBubble(
|
|
60
|
+
paragraph("A chat has been shared with you."),
|
|
61
|
+
chatUserBubble(chatTitle) + previewHtml,
|
|
77
62
|
chatDots(),
|
|
78
63
|
ctaBox({
|
|
79
64
|
label: "SEE MORE",
|
|
@@ -83,7 +68,7 @@ export function renderChatSharedHtml(payload: ChatSharedPayload): string {
|
|
|
83
68
|
maxWidth: "200px",
|
|
84
69
|
}),
|
|
85
70
|
footer(`This share was sent via ${COMPANY_NAME}.`),
|
|
86
|
-
|
|
87
|
-
|
|
71
|
+
paragraph(NOTICE, "tight"),
|
|
72
|
+
signature(),
|
|
88
73
|
]);
|
|
89
74
|
}
|
|
@@ -8,7 +8,7 @@ import { baseTextLayout } from "./base-text";
|
|
|
8
8
|
import {
|
|
9
9
|
ACCESS_PHRASE,
|
|
10
10
|
asciiCtaBox,
|
|
11
|
-
|
|
11
|
+
paragraph,
|
|
12
12
|
ctaBox,
|
|
13
13
|
footer,
|
|
14
14
|
greeting,
|
|
@@ -48,7 +48,7 @@ export function renderAccessApprovedHtml(
|
|
|
48
48
|
|
|
49
49
|
return htmlShell([
|
|
50
50
|
greeting(),
|
|
51
|
-
|
|
51
|
+
paragraph(
|
|
52
52
|
`<b>${escapeHtml(approverName)}</b> approved your request to access <b>"${escapeHtml(docTitle)}"</b> — you ${ACCESS_PHRASE[accessLevel]}.`,
|
|
53
53
|
),
|
|
54
54
|
ctaBox({
|
|
@@ -6,12 +6,12 @@ import type { EmailPayloads } from "../types";
|
|
|
6
6
|
|
|
7
7
|
import { baseTextLayout } from "./base-text";
|
|
8
8
|
import {
|
|
9
|
-
|
|
9
|
+
paragraph,
|
|
10
10
|
footer,
|
|
11
11
|
greeting,
|
|
12
12
|
htmlShell,
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
chatUserBubble,
|
|
14
|
+
chatUserBoxText,
|
|
15
15
|
} from "./blocks";
|
|
16
16
|
import { COMPANY_NAME } from "./constants";
|
|
17
17
|
import { escapeHtml } from "./escape-html";
|
|
@@ -31,7 +31,7 @@ export function renderAccessDeniedBody(payload: AccessDeniedPayload): string {
|
|
|
31
31
|
|
|
32
32
|
if (reason) {
|
|
33
33
|
sections.push("");
|
|
34
|
-
sections.push(
|
|
34
|
+
sections.push(chatUserBoxText(reason, `Reason from ${approverName}`));
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
sections.push("");
|
|
@@ -45,15 +45,10 @@ export function renderAccessDeniedHtml(payload: AccessDeniedPayload): string {
|
|
|
45
45
|
|
|
46
46
|
return htmlShell([
|
|
47
47
|
greeting(),
|
|
48
|
-
|
|
48
|
+
paragraph(
|
|
49
49
|
`<b>${escapeHtml(approverName)}</b> declined your request to access <b>"${escapeHtml(docTitle)}"</b>.`,
|
|
50
50
|
),
|
|
51
|
-
reason
|
|
52
|
-
? quoteBlock(
|
|
53
|
-
`Reason from ${escapeHtml(approverName)}:`,
|
|
54
|
-
escapeHtml(reason),
|
|
55
|
-
)
|
|
56
|
-
: "",
|
|
51
|
+
reason ? chatUserBubble(reason, `Reason from ${approverName}`) : "",
|
|
57
52
|
footer(
|
|
58
53
|
`This notification was sent via ${COMPANY_NAME}.`,
|
|
59
54
|
undefined,
|
|
@@ -7,14 +7,14 @@ import type { EmailPayloads } from "../types";
|
|
|
7
7
|
import { baseTextLayout } from "./base-text";
|
|
8
8
|
import {
|
|
9
9
|
asciiCtaBox,
|
|
10
|
-
|
|
10
|
+
paragraph,
|
|
11
11
|
ctaBox,
|
|
12
12
|
footer,
|
|
13
13
|
greeting,
|
|
14
14
|
htmlShell,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
chatUserBubble,
|
|
16
|
+
chatUserBoxText,
|
|
17
|
+
signature,
|
|
18
18
|
} from "./blocks";
|
|
19
19
|
import { COMPANY_NAME } from "./constants";
|
|
20
20
|
import { escapeHtml } from "./escape-html";
|
|
@@ -36,7 +36,7 @@ export function renderAccessRequestedBody(
|
|
|
36
36
|
|
|
37
37
|
if (message) {
|
|
38
38
|
sections.push("");
|
|
39
|
-
sections.push(
|
|
39
|
+
sections.push(chatUserBoxText(message, `Message from ${requesterName}`));
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
sections.push("");
|
|
@@ -57,15 +57,10 @@ export function renderAccessRequestedHtml(
|
|
|
57
57
|
|
|
58
58
|
return htmlShell([
|
|
59
59
|
greeting(),
|
|
60
|
-
|
|
60
|
+
paragraph(
|
|
61
61
|
`<b>${escapeHtml(requesterName)}</b> is requesting access to <b>"${escapeHtml(docTitle)}"</b>.`,
|
|
62
62
|
),
|
|
63
|
-
message
|
|
64
|
-
? quoteBlock(
|
|
65
|
-
`Message from ${escapeHtml(requesterName)}:`,
|
|
66
|
-
escapeHtml(message),
|
|
67
|
-
)
|
|
68
|
-
: "",
|
|
63
|
+
message ? chatUserBubble(message, `Message from ${requesterName}`) : "",
|
|
69
64
|
ctaBox({
|
|
70
65
|
label: "REVIEW",
|
|
71
66
|
href: reviewUrl,
|
|
@@ -74,6 +69,6 @@ export function renderAccessRequestedHtml(
|
|
|
74
69
|
maxWidth: "220px",
|
|
75
70
|
}),
|
|
76
71
|
footer(`This notification was sent via ${COMPANY_NAME}.`, OWNER_NOTE),
|
|
77
|
-
|
|
72
|
+
signature(),
|
|
78
73
|
]);
|
|
79
74
|
}
|
|
@@ -16,9 +16,3 @@ export const SUPPORT_EMAIL = "support@companysemantics.ai";
|
|
|
16
16
|
/** Monospace font stack for HTML emails. */
|
|
17
17
|
export const MONO_FONT_STACK =
|
|
18
18
|
"'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace";
|
|
19
|
-
|
|
20
|
-
/** Max chat-title length before truncation (chat-shared). */
|
|
21
|
-
export const TITLE_TRUNCATE_LENGTH = 30;
|
|
22
|
-
|
|
23
|
-
/** Max preview-text length before truncation (chat-shared). */
|
|
24
|
-
export const PREVIEW_TRUNCATE_LENGTH = 70;
|
|
@@ -6,13 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
// Constants
|
|
9
|
-
export {
|
|
10
|
-
COMPANY_NAME,
|
|
11
|
-
SUPPORT_EMAIL,
|
|
12
|
-
MONO_FONT_STACK,
|
|
13
|
-
TITLE_TRUNCATE_LENGTH,
|
|
14
|
-
PREVIEW_TRUNCATE_LENGTH,
|
|
15
|
-
} from "./constants";
|
|
9
|
+
export { COMPANY_NAME, SUPPORT_EMAIL, MONO_FONT_STACK } from "./constants";
|
|
16
10
|
|
|
17
11
|
// Primitives
|
|
18
12
|
export { escapeHtml } from "./escape-html";
|
|
@@ -21,25 +15,23 @@ export { baseTextLayout } from "./base-text";
|
|
|
21
15
|
// Shared building blocks
|
|
22
16
|
export {
|
|
23
17
|
htmlShell,
|
|
24
|
-
bodyParagraph,
|
|
25
18
|
greeting,
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
paragraph,
|
|
20
|
+
keyValue,
|
|
28
21
|
footer,
|
|
29
|
-
|
|
22
|
+
signature,
|
|
30
23
|
NOTICE,
|
|
31
24
|
ACCESS_PHRASE,
|
|
32
25
|
chatDots,
|
|
33
26
|
ctaBox,
|
|
34
|
-
quoteBlock,
|
|
35
27
|
chatUserBubble,
|
|
36
28
|
chatAssistantBubble,
|
|
37
29
|
asciiCtaBox,
|
|
38
30
|
asciiCodeBox,
|
|
39
|
-
textQuote,
|
|
40
31
|
chatDotsText,
|
|
41
32
|
chatUserBoxText,
|
|
42
33
|
chatAssistantBoxText,
|
|
34
|
+
clampMessage,
|
|
43
35
|
type Spacing,
|
|
44
36
|
type CtaBoxOptions,
|
|
45
37
|
} from "./blocks";
|
|
@@ -57,6 +49,7 @@ export {
|
|
|
57
49
|
export {
|
|
58
50
|
renderAuthOtpBody,
|
|
59
51
|
renderAuthOtpHtml,
|
|
52
|
+
type AuthOtpPayload,
|
|
60
53
|
type RenderOptions,
|
|
61
54
|
} from "./auth-otp";
|
|
62
55
|
export { renderOrgInviteBody, renderOrgInviteHtml } from "./org-invite";
|
|
@@ -7,13 +7,13 @@ import type { EmailPayloads } from "../types";
|
|
|
7
7
|
import { baseTextLayout } from "./base-text";
|
|
8
8
|
import {
|
|
9
9
|
asciiCtaBox,
|
|
10
|
-
|
|
10
|
+
paragraph,
|
|
11
11
|
ctaBox,
|
|
12
12
|
footer,
|
|
13
13
|
htmlShell,
|
|
14
|
-
|
|
14
|
+
keyValue,
|
|
15
15
|
NOTICE,
|
|
16
|
-
|
|
16
|
+
signature,
|
|
17
17
|
} from "./blocks";
|
|
18
18
|
import { COMPANY_NAME } from "./constants";
|
|
19
19
|
import { escapeHtml } from "./escape-html";
|
|
@@ -27,13 +27,13 @@ export function renderOrgInviteBody(payload: OrgInvitePayload): string {
|
|
|
27
27
|
sections.push("Workspace invitation.");
|
|
28
28
|
sections.push(asciiCtaBox("JOIN"));
|
|
29
29
|
sections.push("");
|
|
30
|
+
sections.push(acceptUrl);
|
|
31
|
+
sections.push("");
|
|
30
32
|
sections.push(`From: ${inviterName}`);
|
|
31
33
|
sections.push(`Workspace: ${orgName}`);
|
|
32
34
|
sections.push(`Role: ${role}`);
|
|
33
35
|
sections.push(`Expires in: ${expiresInDays} days`);
|
|
34
36
|
sections.push("");
|
|
35
|
-
sections.push(acceptUrl);
|
|
36
|
-
sections.push("");
|
|
37
37
|
sections.push(`This invitation was sent via ${COMPANY_NAME}.`);
|
|
38
38
|
sections.push(NOTICE);
|
|
39
39
|
|
|
@@ -44,18 +44,18 @@ export function renderOrgInviteHtml(payload: OrgInvitePayload): string {
|
|
|
44
44
|
const { inviterName, orgName, role, acceptUrl, expiresInDays } = payload;
|
|
45
45
|
|
|
46
46
|
return htmlShell([
|
|
47
|
-
|
|
47
|
+
paragraph("Workspace invitation."),
|
|
48
48
|
ctaBox({
|
|
49
49
|
label: "JOIN",
|
|
50
50
|
href: acceptUrl,
|
|
51
51
|
padding: "20px 24px",
|
|
52
52
|
fontSize: "16px",
|
|
53
53
|
}),
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
keyValue("From", escapeHtml(inviterName), "tight"),
|
|
55
|
+
keyValue("Workspace", escapeHtml(orgName), "tight"),
|
|
56
|
+
keyValue("Role", role, "tight"),
|
|
57
|
+
keyValue("Expires in", `${expiresInDays} days`, "normal"),
|
|
58
58
|
footer(`This invitation was sent via ${COMPANY_NAME}.`, NOTICE),
|
|
59
|
-
|
|
59
|
+
signature(),
|
|
60
60
|
]);
|
|
61
61
|
}
|