@company-semantics/contracts 27.14.0 → 28.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,56 +1,35 @@
1
1
  /**
2
- * Company.md access-approved email (sent to the requester). Text + HTML.
2
+ * Company.md access-approved email (sent to the requester).
3
3
  */
4
4
 
5
5
  import type { EmailPayloads } from "../types";
6
6
 
7
- import { baseTextLayout } from "./base-text";
8
7
  import {
9
8
  ACCESS_PHRASE,
10
- asciiCtaBox,
11
- paragraph,
9
+ type Block,
10
+ bold,
12
11
  ctaBox,
13
12
  footer,
14
13
  greeting,
15
- htmlShell,
14
+ paragraph,
15
+ signature,
16
16
  } from "./blocks";
17
17
  import { COMPANY_NAME } from "./constants";
18
- import { escapeHtml } from "./escape-html";
19
18
 
20
19
  export type AccessApprovedPayload =
21
20
  EmailPayloads["companyMd.access_request_approved"];
22
21
 
23
- export function renderAccessApprovedBody(
24
- payload: AccessApprovedPayload,
25
- ): string {
26
- const { approverName, docTitle, accessLevel, docUrl } = payload;
27
-
28
- const sections: string[] = [];
29
- sections.push("Hi,");
30
- sections.push("");
31
- sections.push(
32
- `${approverName} approved your request to access "${docTitle}" — you ${ACCESS_PHRASE[accessLevel]}.`,
33
- );
34
- sections.push("");
35
- sections.push(asciiCtaBox("OPEN"));
36
- sections.push("");
37
- sections.push(docUrl);
38
- sections.push("");
39
- sections.push(`This notification was sent via ${COMPANY_NAME}.`);
40
-
41
- return baseTextLayout(sections.join("\n"));
42
- }
43
-
44
- export function renderAccessApprovedHtml(
45
- payload: AccessApprovedPayload,
46
- ): string {
22
+ export function renderAccessApproved(payload: AccessApprovedPayload): Block[] {
47
23
  const { approverName, docTitle, accessLevel, docUrl } = payload;
48
24
 
49
- return htmlShell([
25
+ return [
50
26
  greeting(),
51
- paragraph(
52
- `<b>${escapeHtml(approverName)}</b> approved your request to access <b>"${escapeHtml(docTitle)}"</b> &mdash; you ${ACCESS_PHRASE[accessLevel]}.`,
53
- ),
27
+ paragraph([
28
+ bold(approverName),
29
+ " approved your request to access ",
30
+ bold(`"${docTitle}"`),
31
+ ` — you ${ACCESS_PHRASE[accessLevel]}.`,
32
+ ]),
54
33
  ctaBox({
55
34
  label: "OPEN",
56
35
  href: docUrl,
@@ -58,10 +37,7 @@ export function renderAccessApprovedHtml(
58
37
  fontSize: "16px",
59
38
  maxWidth: "220px",
60
39
  }),
61
- footer(
62
- `This notification was sent via ${COMPANY_NAME}.`,
63
- undefined,
64
- "none",
65
- ),
66
- ]);
40
+ footer(`This notification was sent via ${COMPANY_NAME}.`),
41
+ signature(),
42
+ ];
67
43
  }
@@ -1,58 +1,42 @@
1
1
  /**
2
- * Company.md access-denied email (sent to the requester). No CTA. Text + HTML.
2
+ * Company.md access-denied email (sent to the requester). No CTA.
3
3
  */
4
4
 
5
5
  import type { EmailPayloads } from "../types";
6
6
 
7
- import { baseTextLayout } from "./base-text";
8
7
  import {
9
- paragraph,
8
+ type Block,
9
+ bold,
10
+ chatUser,
10
11
  footer,
11
12
  greeting,
12
- htmlShell,
13
- chatUserBubble,
14
- chatUserBoxText,
13
+ paragraph,
14
+ signature,
15
15
  } from "./blocks";
16
16
  import { COMPANY_NAME } from "./constants";
17
- import { escapeHtml } from "./escape-html";
18
17
 
19
18
  export type AccessDeniedPayload =
20
19
  EmailPayloads["companyMd.access_request_denied"];
21
20
 
22
- export function renderAccessDeniedBody(payload: AccessDeniedPayload): string {
21
+ export function renderAccessDenied(payload: AccessDeniedPayload): Block[] {
23
22
  const { approverName, docTitle, reason } = payload;
24
23
 
25
- const sections: string[] = [];
26
- sections.push("Hi,");
27
- sections.push("");
28
- sections.push(
29
- `${approverName} declined your request to access "${docTitle}".`,
24
+ const blocks: Block[] = [
25
+ greeting(),
26
+ paragraph([
27
+ bold(approverName),
28
+ " declined your request to access ",
29
+ bold(`"${docTitle}"`),
30
+ ".",
31
+ ]),
32
+ ];
33
+
34
+ if (reason) blocks.push(chatUser(reason, `Reason from ${approverName}`));
35
+
36
+ blocks.push(
37
+ footer(`This notification was sent via ${COMPANY_NAME}.`),
38
+ signature(),
30
39
  );
31
40
 
32
- if (reason) {
33
- sections.push("");
34
- sections.push(chatUserBoxText(reason, `Reason from ${approverName}`));
35
- }
36
-
37
- sections.push("");
38
- sections.push(`This notification was sent via ${COMPANY_NAME}.`);
39
-
40
- return baseTextLayout(sections.join("\n"));
41
- }
42
-
43
- export function renderAccessDeniedHtml(payload: AccessDeniedPayload): string {
44
- const { approverName, docTitle, reason } = payload;
45
-
46
- return htmlShell([
47
- greeting(),
48
- paragraph(
49
- `<b>${escapeHtml(approverName)}</b> declined your request to access <b>"${escapeHtml(docTitle)}"</b>.`,
50
- ),
51
- reason ? chatUserBubble(reason, `Reason from ${approverName}`) : "",
52
- footer(
53
- `This notification was sent via ${COMPANY_NAME}.`,
54
- undefined,
55
- "none",
56
- ),
57
- ]);
41
+ return blocks;
58
42
  }
@@ -1,66 +1,44 @@
1
1
  /**
2
- * Company.md access-requested email (sent to the doc owner). Text + HTML.
2
+ * Company.md access-requested email (sent to the doc owner).
3
3
  */
4
4
 
5
5
  import type { EmailPayloads } from "../types";
6
6
 
7
- import { baseTextLayout } from "./base-text";
8
7
  import {
9
- asciiCtaBox,
10
- paragraph,
8
+ type Block,
9
+ bold,
10
+ chatUser,
11
11
  ctaBox,
12
12
  footer,
13
13
  greeting,
14
- htmlShell,
15
- chatUserBubble,
16
- chatUserBoxText,
14
+ paragraph,
17
15
  signature,
18
16
  } from "./blocks";
19
17
  import { COMPANY_NAME } from "./constants";
20
- import { escapeHtml } from "./escape-html";
21
18
 
22
19
  export type AccessRequestedPayload =
23
20
  EmailPayloads["companyMd.access_requested"];
24
21
 
25
22
  const OWNER_NOTE = "You are receiving this because you own this document.";
26
23
 
27
- export function renderAccessRequestedBody(
24
+ export function renderAccessRequested(
28
25
  payload: AccessRequestedPayload,
29
- ): string {
26
+ ): Block[] {
30
27
  const { requesterName, docTitle, message, reviewUrl } = payload;
31
28
 
32
- const sections: string[] = [];
33
- sections.push("Hi,");
34
- sections.push("");
35
- sections.push(`${requesterName} is requesting access to "${docTitle}".`);
36
-
37
- if (message) {
38
- sections.push("");
39
- sections.push(chatUserBoxText(message, `Message from ${requesterName}`));
40
- }
41
-
42
- sections.push("");
43
- sections.push(asciiCtaBox("REVIEW"));
44
- sections.push("");
45
- sections.push(reviewUrl);
46
- sections.push("");
47
- sections.push(`This notification was sent via ${COMPANY_NAME}.`);
48
- sections.push(OWNER_NOTE);
49
-
50
- return baseTextLayout(sections.join("\n"));
51
- }
29
+ const blocks: Block[] = [
30
+ greeting(),
31
+ paragraph([
32
+ bold(requesterName),
33
+ " is requesting access to ",
34
+ bold(`"${docTitle}"`),
35
+ ".",
36
+ ]),
37
+ ];
52
38
 
53
- export function renderAccessRequestedHtml(
54
- payload: AccessRequestedPayload,
55
- ): string {
56
- const { requesterName, docTitle, message, reviewUrl } = payload;
39
+ if (message) blocks.push(chatUser(message, `Message from ${requesterName}`));
57
40
 
58
- return htmlShell([
59
- greeting(),
60
- paragraph(
61
- `<b>${escapeHtml(requesterName)}</b> is requesting access to <b>"${escapeHtml(docTitle)}"</b>.`,
62
- ),
63
- message ? chatUserBubble(message, `Message from ${requesterName}`) : "",
41
+ blocks.push(
64
42
  ctaBox({
65
43
  label: "REVIEW",
66
44
  href: reviewUrl,
@@ -70,5 +48,7 @@ export function renderAccessRequestedHtml(
70
48
  }),
71
49
  footer(`This notification was sent via ${COMPANY_NAME}.`, OWNER_NOTE),
72
50
  signature(),
73
- ]);
51
+ );
52
+
53
+ return blocks;
74
54
  }
@@ -10,13 +10,13 @@ export { COMPANY_NAME, SUPPORT_EMAIL, MONO_FONT_STACK } from "./constants";
10
10
 
11
11
  // Primitives
12
12
  export { escapeHtml } from "./escape-html";
13
- export { baseTextLayout } from "./base-text";
14
13
 
15
- // Shared building blocks
14
+ // Dual-output building blocks (each returns a `Block` = { html, text })
16
15
  export {
17
16
  htmlShell,
18
- greeting,
17
+ textShell,
19
18
  paragraph,
19
+ greeting,
20
20
  keyValue,
21
21
  footer,
22
22
  signature,
@@ -24,14 +24,12 @@ export {
24
24
  ACCESS_PHRASE,
25
25
  chatDots,
26
26
  ctaBox,
27
- chatUserBubble,
28
- chatAssistantBubble,
29
- asciiCtaBox,
30
- asciiCodeBox,
31
- chatDotsText,
32
- chatUserBoxText,
33
- chatAssistantBoxText,
34
- clampMessage,
27
+ chatUser,
28
+ chatAssistant,
29
+ bold,
30
+ type Block,
31
+ type Inline,
32
+ type InlineContent,
35
33
  type Spacing,
36
34
  type CtaBoxOptions,
37
35
  } from "./blocks";
@@ -45,39 +43,6 @@ export {
45
43
  type RenderEmailOptions,
46
44
  } from "./render-email";
47
45
 
48
- // Templates
49
- export {
50
- renderAuthOtpBody,
51
- renderAuthOtpHtml,
52
- type AuthOtpPayload,
53
- type RenderOptions,
54
- } from "./auth-otp";
55
- export { renderOrgInviteBody, renderOrgInviteHtml } from "./org-invite";
56
- export { renderChatSharedBody, renderChatSharedHtml } from "./chat-shared";
57
- export {
58
- renderSecurityAlertBody,
59
- SECURITY_ALERT_TYPES,
60
- type SecurityAlertType,
61
- } from "./security-alert";
62
- export {
63
- renderUnitOwnerGrantedBody,
64
- renderUnitOwnerGrantedHtml,
65
- } from "./unit-owner-granted";
66
- export {
67
- renderShareGrantedBody,
68
- renderShareGrantedHtml,
69
- } from "./share-granted";
70
- export { renderOwnershipTransferBody } from "./ownership-transfer";
71
- export { renderOwnershipTransferCompletedBody } from "./ownership-transfer-completed";
72
- export {
73
- renderAccessRequestedBody,
74
- renderAccessRequestedHtml,
75
- } from "./company-md-access-requested";
76
- export {
77
- renderAccessApprovedBody,
78
- renderAccessApprovedHtml,
79
- } from "./company-md-access-approved";
80
- export {
81
- renderAccessDeniedBody,
82
- renderAccessDeniedHtml,
83
- } from "./company-md-access-denied";
46
+ // Template payload types + alert vocabulary
47
+ export type { AuthOtpPayload, RenderOptions } from "./auth-otp";
48
+ export { SECURITY_ALERT_TYPES, type SecurityAlertType } from "./security-alert";
@@ -1,49 +1,26 @@
1
1
  /**
2
- * Org invite email (join a workspace). Plain text + HTML.
2
+ * Org invite email (join a workspace).
3
3
  */
4
4
 
5
5
  import type { EmailPayloads } from "../types";
6
6
 
7
- import { baseTextLayout } from "./base-text";
8
7
  import {
9
- asciiCtaBox,
10
- paragraph,
8
+ type Block,
11
9
  ctaBox,
12
10
  footer,
13
- htmlShell,
14
11
  keyValue,
15
12
  NOTICE,
13
+ paragraph,
16
14
  signature,
17
15
  } from "./blocks";
18
16
  import { COMPANY_NAME } from "./constants";
19
- import { escapeHtml } from "./escape-html";
20
17
 
21
18
  export type OrgInvitePayload = EmailPayloads["org.invite"];
22
19
 
23
- export function renderOrgInviteBody(payload: OrgInvitePayload): string {
24
- const { inviterName, orgName, role, acceptUrl, expiresInDays } = payload;
25
-
26
- const sections: string[] = [];
27
- sections.push("Workspace invitation.");
28
- sections.push(asciiCtaBox("JOIN"));
29
- sections.push("");
30
- sections.push(acceptUrl);
31
- sections.push("");
32
- sections.push(`From: ${inviterName}`);
33
- sections.push(`Workspace: ${orgName}`);
34
- sections.push(`Role: ${role}`);
35
- sections.push(`Expires in: ${expiresInDays} days`);
36
- sections.push("");
37
- sections.push(`This invitation was sent via ${COMPANY_NAME}.`);
38
- sections.push(NOTICE);
39
-
40
- return baseTextLayout(sections.join("\n"));
41
- }
42
-
43
- export function renderOrgInviteHtml(payload: OrgInvitePayload): string {
20
+ export function renderOrgInvite(payload: OrgInvitePayload): Block[] {
44
21
  const { inviterName, orgName, role, acceptUrl, expiresInDays } = payload;
45
22
 
46
- return htmlShell([
23
+ return [
47
24
  paragraph("Workspace invitation."),
48
25
  ctaBox({
49
26
  label: "JOIN",
@@ -51,11 +28,11 @@ export function renderOrgInviteHtml(payload: OrgInvitePayload): string {
51
28
  padding: "20px 24px",
52
29
  fontSize: "16px",
53
30
  }),
54
- keyValue("From", escapeHtml(inviterName), "tight"),
55
- keyValue("Workspace", escapeHtml(orgName), "tight"),
56
- keyValue("Role", role, "tight"),
31
+ keyValue("From", inviterName),
32
+ keyValue("Workspace", orgName),
33
+ keyValue("Role", role),
57
34
  keyValue("Expires in", `${expiresInDays} days`, "normal"),
58
35
  footer(`This invitation was sent via ${COMPANY_NAME}.`, NOTICE),
59
36
  signature(),
60
- ]);
37
+ ];
61
38
  }
@@ -4,28 +4,30 @@
4
4
 
5
5
  import type { EmailPayloads } from "../types";
6
6
 
7
- import { baseTextLayout } from "./base-text";
7
+ import { type Block, bold, footer, paragraph, signature } from "./blocks";
8
8
 
9
9
  export type OwnershipTransferCompletedPayload =
10
10
  EmailPayloads["org.ownership_transfer_completed"];
11
11
 
12
- export function renderOwnershipTransferCompletedBody(
12
+ export function renderOwnershipTransferCompleted(
13
13
  payload: OwnershipTransferCompletedPayload,
14
- ): string {
14
+ ): Block[] {
15
15
  const { orgName, newOwnerEmail } = payload;
16
16
 
17
- const sections: string[] = [];
18
- sections.push(
19
- `The ownership of "${orgName}" has been transferred to ${newOwnerEmail}.`,
20
- );
21
- sections.push("");
22
- sections.push(
23
- "You are no longer the owner of this workspace, but you remain an admin.",
24
- );
25
- sections.push("");
26
- sections.push(
27
- "If you did not authorize this transfer, please contact support immediately.",
28
- );
29
-
30
- return baseTextLayout(sections.join("\n"));
17
+ return [
18
+ paragraph([
19
+ "The ownership of ",
20
+ bold(`"${orgName}"`),
21
+ " has been transferred to ",
22
+ bold(newOwnerEmail),
23
+ ".",
24
+ ]),
25
+ paragraph(
26
+ "You are no longer the owner of this workspace, but you remain an admin.",
27
+ ),
28
+ footer(
29
+ "If you did not authorize this transfer, please contact support immediately.",
30
+ ),
31
+ signature(),
32
+ ];
31
33
  }
@@ -4,33 +4,48 @@
4
4
 
5
5
  import type { EmailPayloads } from "../types";
6
6
 
7
- import { baseTextLayout } from "./base-text";
8
- import { chatUserBoxText } from "./blocks";
7
+ import {
8
+ type Block,
9
+ bold,
10
+ chatUser,
11
+ ctaBox,
12
+ footer,
13
+ keyValue,
14
+ paragraph,
15
+ signature,
16
+ } from "./blocks";
9
17
 
10
18
  export type OwnershipTransferPayload = EmailPayloads["org.ownership_transfer"];
11
19
 
12
- export function renderOwnershipTransferBody(
20
+ export function renderOwnershipTransfer(
13
21
  payload: OwnershipTransferPayload,
14
- ): string {
22
+ ): Block[] {
15
23
  const { orgName, acceptUrl, expiresInDays, note } = payload;
16
24
 
17
- const sections: string[] = [];
18
- sections.push(`You have been invited to become the owner of "${orgName}".`);
19
-
20
- if (note) {
21
- sections.push("");
22
- sections.push(chatUserBoxText(note, "Message from the current owner"));
23
- }
24
-
25
- sections.push("");
26
- sections.push("To accept this invitation, please click the link below:");
27
- sections.push(acceptUrl);
28
- sections.push("");
29
- sections.push(`This invitation will expire in ${expiresInDays} days.`);
30
- sections.push("");
31
- sections.push(
32
- "If you did not expect this invitation, you can safely ignore this email.",
25
+ const blocks: Block[] = [
26
+ paragraph([
27
+ "You have been invited to become the owner of ",
28
+ bold(`"${orgName}"`),
29
+ ".",
30
+ ]),
31
+ ];
32
+
33
+ if (note) blocks.push(chatUser(note, "Message from the current owner"));
34
+
35
+ blocks.push(
36
+ ctaBox({
37
+ label: "ACCEPT",
38
+ href: acceptUrl,
39
+ padding: "16px 24px",
40
+ fontSize: "16px",
41
+ maxWidth: "220px",
42
+ }),
43
+ keyValue("Expires in", `${expiresInDays} days`, "normal"),
44
+ footer(
45
+ "If you did not expect this invitation, you can safely ignore this email.",
46
+ ),
47
+ signature(),
33
48
  );
34
49
 
35
- return baseTextLayout(sections.join("\n"));
50
+ return blocks;
36
51
  }