@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.
@@ -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
- bodyParagraph,
14
+ paragraph,
15
15
  ctaBox,
16
16
  footer,
17
17
  htmlShell,
18
- metaRow,
19
- monoParagraph,
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
- bodyParagraph("Copy/paste code in login form:"),
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
- metaRow("Status", "VALID", "tight"),
79
- metaRow(
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
- monoParagraph("If this wasn't you, no action is required."),
89
- signatureLine(),
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
- /** Intro / greeting / body paragraph (inherits the body monospace). */
54
- export function bodyParagraph(html: string): string {
55
- return `<p style="margin: 0 0 20px 0; font-size: 14px;">${html}</p>`;
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 bodyParagraph(
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>" metadata row. */
74
- export function metaRow(
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 monoParagraph(`${label}: <b>${valueHtml}</b>`, spacing);
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 monoParagraph(
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 signatureLine(): string {
96
- return monoParagraph(COMPANY_NAME, "none");
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
- /** Right-aligned user chat bubble. `contentHtml` must be escaped + truncated. */
189
- export function chatUserBubble(contentHtml: string): string {
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. `contentHtml` must be escaped + truncated. */
199
- export function chatAssistantBubble(contentHtml: string): string {
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: top;">[c_S]</td>
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
- /** Normalized `>> LABEL <<` ASCII box (`*` corners, centered). */
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 line = `>> ${label} <<`;
225
- const border = `*${"-".repeat(CTA_BOX_WIDTH)}*`;
226
- const blank = `|${" ".repeat(CTA_BOX_WIDTH)}|`;
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 — quote + chat boxes
240
+ // Plain text — chat boxes
251
241
  // =============================================================================
252
242
 
253
- /** Plain-text quoted message: `Label:` then the quoted value. */
254
- export function textQuote(label: string, value: string): string {
255
- return `${label}:\n"${value}"`;
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
- /** Right-aligned user chat box (title, kaomoji). */
264
- export function chatUserBoxText(title: string): string {
265
- const truncated =
266
- title.length > TITLE_TRUNCATE_LENGTH
267
- ? title.slice(0, TITLE_TRUNCATE_LENGTH - 3) + "..."
268
- : title;
269
- const padded = truncated.padStart(35);
270
- return `┌────────────────────────────────────┐
271
- │${padded} │ (•̀_ರ╮)
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 (max 2 lines, 36 wide). */
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 maxChars = lineWidth * 2;
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
- const bot = " └──────────────────────────────────────┘";
301
- const boxLines = lines.map((line, i) => {
302
- const padded = ` ${line.padEnd(lineWidth)} `;
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
- bodyParagraph,
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
- signatureLine,
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 title = escapeHtml(truncate(chatTitle, TITLE_TRUNCATE_LENGTH));
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
- bodyParagraph("A chat has been shared with you."),
76
- chatUserBubble(title) + previewHtml,
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
- monoParagraph(NOTICE, "tight"),
87
- signatureLine(),
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
- bodyParagraph,
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
- bodyParagraph(
51
+ paragraph(
52
52
  `<b>${escapeHtml(approverName)}</b> approved your request to access <b>"${escapeHtml(docTitle)}"</b> &mdash; 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
- bodyParagraph,
9
+ paragraph,
10
10
  footer,
11
11
  greeting,
12
12
  htmlShell,
13
- quoteBlock,
14
- textQuote,
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(textQuote(`Reason from ${approverName}`, reason));
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
- bodyParagraph(
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
- bodyParagraph,
10
+ paragraph,
11
11
  ctaBox,
12
12
  footer,
13
13
  greeting,
14
14
  htmlShell,
15
- quoteBlock,
16
- signatureLine,
17
- textQuote,
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(textQuote(`Message from ${requesterName}`, message));
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
- bodyParagraph(
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
- signatureLine(),
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
- monoParagraph,
27
- metaRow,
19
+ paragraph,
20
+ keyValue,
28
21
  footer,
29
- signatureLine,
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
- bodyParagraph,
10
+ paragraph,
11
11
  ctaBox,
12
12
  footer,
13
13
  htmlShell,
14
- metaRow,
14
+ keyValue,
15
15
  NOTICE,
16
- signatureLine,
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
- bodyParagraph("Workspace invitation."),
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
- metaRow("From", escapeHtml(inviterName), "tight"),
55
- metaRow("Workspace", escapeHtml(orgName), "tight"),
56
- metaRow("Role", role, "tight"),
57
- metaRow("Expires in", `${expiresInDays} days`, "normal"),
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
- signatureLine(),
59
+ signature(),
60
60
  ]);
61
61
  }