@company-semantics/contracts 52.0.0 → 53.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +122 -69
- package/src/generated/openapi-routes.ts +3 -1
- package/src/identity/README.md +2 -2
- package/src/identity/__tests__/people-org-chart.test.ts +52 -17
- package/src/identity/__tests__/position-ref.test.ts +44 -0
- package/src/identity/index.ts +6 -2
- package/src/identity/people-org-chart.ts +26 -15
- package/src/identity/position-ref.ts +24 -0
- package/src/index.ts +21 -2
- package/src/notifications/__tests__/__snapshots__/monospace-budget.test.ts.snap +23 -0
- package/src/notifications/__tests__/__snapshots__/render-snapshot.test.ts.snap +309 -259
- package/src/notifications/__tests__/monospace-budget.test.ts +75 -0
- package/src/notifications/renderers/README.md +8 -4
- package/src/notifications/renderers/ascii/README.md +75 -0
- package/src/notifications/renderers/ascii/__tests__/README.md +39 -0
- package/src/notifications/renderers/ascii/__tests__/layout.test.ts +228 -0
- package/src/notifications/renderers/ascii/chat.ts +179 -0
- package/src/notifications/renderers/ascii/cta.ts +57 -0
- package/src/notifications/renderers/ascii/geometry.ts +112 -0
- package/src/notifications/renderers/ascii/index.ts +40 -0
- package/src/notifications/renderers/ascii/keyvalue.ts +34 -0
- package/src/notifications/renderers/ascii/rule.ts +53 -0
- package/src/notifications/renderers/ascii/runs.ts +84 -0
- package/src/notifications/renderers/ascii/signature.ts +41 -0
- package/src/notifications/renderers/ascii/wrap.ts +96 -0
- package/src/notifications/renderers/brand.ts +12 -0
- package/src/notifications/renderers/email/chat.ts +62 -146
- package/src/notifications/renderers/email/constants.ts +17 -2
- package/src/notifications/renderers/email/cta.ts +8 -13
- package/src/notifications/renderers/email/render.ts +29 -5
- package/src/notifications/renderers/layout.ts +31 -0
- package/src/notifications/renderers/slack/README.md +135 -79
- package/src/notifications/renderers/slack/__tests__/README.md +3 -2
- package/src/notifications/renderers/slack/__tests__/index.test.ts +233 -93
- package/src/notifications/renderers/slack/blocks.ts +149 -0
- package/src/notifications/renderers/slack/chat.ts +167 -0
- package/src/notifications/renderers/slack/cta.ts +69 -0
- package/src/notifications/renderers/slack/index.ts +136 -229
- package/src/notifications/renderers/slack/message.ts +23 -0
- package/src/org/README.md +10 -2
- package/src/org/__tests__/org-units.test.ts +1 -1
- package/src/org/__tests__/set-seat-manager.test.ts +177 -0
- package/src/org/index.ts +20 -2
- package/src/org/reconciliation.ts +162 -0
- package/src/org/schemas.ts +43 -17
- package/src/identity/org-chart-actor.ts +0 -24
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A `chatUnit` on Slack — the element this channel used to drop on the floor.
|
|
3
|
+
*
|
|
4
|
+
* It was declined whole on the grounds that Block Kit has no vocabulary for
|
|
5
|
+
* turn-taking. That reasoning was sound about BLOCKS and wrong about the
|
|
6
|
+
* message: `auth.otp` and `org.invite` both nest their payload inside a
|
|
7
|
+
* `chatUnit`, so declining the element meant the one-time code and the accept
|
|
8
|
+
* button never reached Slack at all. That is semantic loss, not a presentation
|
|
9
|
+
* limitation, and no amount of correctness about turn-taking justifies posting a
|
|
10
|
+
* login email with no login code in it.
|
|
11
|
+
*
|
|
12
|
+
* Slack does have a monospace surface, so the conversation is drawn the way
|
|
13
|
+
* email draws it — `../ascii`'s bubbles, from the same functions, so the two
|
|
14
|
+
* channels cannot drift — inside `rich_text_preformatted`.
|
|
15
|
+
*
|
|
16
|
+
* **Linked CTAs break OUT of the drawing.** An `actions` block cannot nest
|
|
17
|
+
* inside `rich_text`, and it should not want to: a native button is the better
|
|
18
|
+
* control (see `./cta.ts`). So the stream is cut at each linked CTA, and the
|
|
19
|
+
* button is emitted between the halves. A hrefless CTA — the OTP code — stays
|
|
20
|
+
* inside the drawing, where the box IS the payload.
|
|
21
|
+
*
|
|
22
|
+
* INVARIANTS:
|
|
23
|
+
* - **No empty preformatted block, ever.** A break-out at the head or tail of
|
|
24
|
+
* the stream must not leave a zero-line code box beside the button. Slack
|
|
25
|
+
* renders one as a visible empty rectangle, so this is a drawing bug rather
|
|
26
|
+
* than a tidiness question. Segments are merged and then filtered, so the
|
|
27
|
+
* arithmetic cannot produce one.
|
|
28
|
+
* - **Adjacent buttons coalesce** into one `actions` block rather than one each
|
|
29
|
+
* — see `ctaActions`.
|
|
30
|
+
* - Continuation dots that folded into a LINKED CTA survive the break-out as
|
|
31
|
+
* their own drawn line. Folding them into a button would silently delete them.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import type { KnownBlock } from "@slack/types";
|
|
35
|
+
|
|
36
|
+
import type { CallToAction, ChatUnitItem } from "../../content";
|
|
37
|
+
import {
|
|
38
|
+
type AsciiGeometry,
|
|
39
|
+
ruleAscii,
|
|
40
|
+
type MonospaceLine,
|
|
41
|
+
planChatUnit,
|
|
42
|
+
renderBubbleAscii,
|
|
43
|
+
renderChatCtaAscii,
|
|
44
|
+
renderChatDotsAscii,
|
|
45
|
+
} from "../ascii";
|
|
46
|
+
|
|
47
|
+
import { preformatted } from "./blocks";
|
|
48
|
+
import { ctaActions } from "./cta";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A stretch of the unit that becomes one block.
|
|
52
|
+
*
|
|
53
|
+
* `draw` is monospace lines; `buttons` is a run of linked CTAs. The whole
|
|
54
|
+
* algorithm below is: classify each part into one of these, merge neighbours of
|
|
55
|
+
* the same kind, drop anything empty, emit.
|
|
56
|
+
*/
|
|
57
|
+
type Segment =
|
|
58
|
+
| { kind: "draw"; lines: MonospaceLine[] }
|
|
59
|
+
| { kind: "buttons"; ctas: CallToAction[] };
|
|
60
|
+
|
|
61
|
+
/** Classify the planned parts, keeping drawn lines and buttons apart. */
|
|
62
|
+
function segmentsFor(
|
|
63
|
+
items: ChatUnitItem[],
|
|
64
|
+
geometry: AsciiGeometry,
|
|
65
|
+
): Segment[] {
|
|
66
|
+
const segments: Segment[] = [];
|
|
67
|
+
|
|
68
|
+
for (const part of planChatUnit(items)) {
|
|
69
|
+
if (part.kind === "bubble") {
|
|
70
|
+
segments.push({
|
|
71
|
+
kind: "draw",
|
|
72
|
+
lines: renderBubbleAscii(part.turn, geometry),
|
|
73
|
+
});
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (part.kind === "dots") {
|
|
77
|
+
segments.push({ kind: "draw", lines: renderChatDotsAscii(geometry) });
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (!part.cta.href) {
|
|
81
|
+
// The label is the payload. It belongs in the drawing.
|
|
82
|
+
segments.push({
|
|
83
|
+
kind: "draw",
|
|
84
|
+
lines: renderChatCtaAscii(
|
|
85
|
+
part.cta,
|
|
86
|
+
part.align,
|
|
87
|
+
part.withDots,
|
|
88
|
+
geometry,
|
|
89
|
+
),
|
|
90
|
+
});
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
// A linked CTA leaves the drawing. Its folded dots do not go with it — they
|
|
94
|
+
// are part of the conversation's flow, so they stay drawn.
|
|
95
|
+
if (part.withDots) {
|
|
96
|
+
segments.push({ kind: "draw", lines: renderChatDotsAscii(geometry) });
|
|
97
|
+
}
|
|
98
|
+
segments.push({ kind: "buttons", ctas: [part.cta] });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return segments;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Merge neighbouring segments of the same kind.
|
|
106
|
+
*
|
|
107
|
+
* Drawn stretches rejoin with a blank line between them, the way email joins its
|
|
108
|
+
* parts; button runs concatenate so `ctaActions` can lay them out side by side.
|
|
109
|
+
*/
|
|
110
|
+
function merge(segments: Segment[]): Segment[] {
|
|
111
|
+
const merged: Segment[] = [];
|
|
112
|
+
|
|
113
|
+
for (const segment of segments) {
|
|
114
|
+
const last = merged.at(-1);
|
|
115
|
+
if (last?.kind === "draw" && segment.kind === "draw") {
|
|
116
|
+
last.lines = [...last.lines, [], ...segment.lines];
|
|
117
|
+
} else if (last?.kind === "buttons" && segment.kind === "buttons") {
|
|
118
|
+
last.ctas = [...last.ctas, ...segment.ctas];
|
|
119
|
+
} else {
|
|
120
|
+
merged.push(
|
|
121
|
+
segment.kind === "draw"
|
|
122
|
+
? { kind: "draw", lines: [...segment.lines] }
|
|
123
|
+
: { kind: "buttons", ctas: [...segment.ctas] },
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return merged;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* A conversation as Slack blocks, bracketed by the same `___` rules email draws.
|
|
133
|
+
*
|
|
134
|
+
* The rules join the FIRST and LAST drawn stretches rather than becoming blocks
|
|
135
|
+
* of their own: a unit that opens or closes on a button would otherwise need a
|
|
136
|
+
* one-line code box holding nothing but a rule, which is the empty-block problem
|
|
137
|
+
* wearing a disguise. A unit made entirely of buttons draws no rule at all,
|
|
138
|
+
* because there is no conversation to bracket.
|
|
139
|
+
*/
|
|
140
|
+
export function renderChatUnitBlocks(
|
|
141
|
+
items: ChatUnitItem[],
|
|
142
|
+
geometry: AsciiGeometry,
|
|
143
|
+
): KnownBlock[] {
|
|
144
|
+
const segments = merge(segmentsFor(items, geometry)).filter((segment) =>
|
|
145
|
+
segment.kind === "buttons"
|
|
146
|
+
? segment.ctas.length > 0
|
|
147
|
+
: segment.lines.length > 0,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const drawn = segments.filter((segment) => segment.kind === "draw");
|
|
151
|
+
if (drawn.length > 0) {
|
|
152
|
+
const rule = ruleAscii(
|
|
153
|
+
drawn.flatMap((segment) => segment.lines),
|
|
154
|
+
geometry,
|
|
155
|
+
);
|
|
156
|
+
const first = drawn[0];
|
|
157
|
+
const last = drawn.at(-1);
|
|
158
|
+
first.lines = [rule, [], ...first.lines];
|
|
159
|
+
if (last) last.lines = [...last.lines, rule];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return segments.flatMap((segment): KnownBlock[] =>
|
|
163
|
+
segment.kind === "buttons"
|
|
164
|
+
? ctaActions(segment.ctas)
|
|
165
|
+
: preformatted(segment.lines),
|
|
166
|
+
);
|
|
167
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A `callToAction` on Slack — and the one place this channel deliberately
|
|
3
|
+
* refuses to copy email's drawing.
|
|
4
|
+
*
|
|
5
|
+
* **With an `href`: a native `actions` button.** Slack has a real control here
|
|
6
|
+
* and it is better than anything we could draw — a large touch target, an
|
|
7
|
+
* unambiguous affordance, keyboard and screen-reader semantics supplied by the
|
|
8
|
+
* client, and forward compatibility with whatever Slack's button becomes next.
|
|
9
|
+
* Drawing an ASCII box with a link inside would be sharing the CONTROL
|
|
10
|
+
* REPRESENTATION, which is not what cross-channel consistency means. The
|
|
11
|
+
* vocabulary is shared; the control is the channel's own.
|
|
12
|
+
*
|
|
13
|
+
* **Without an `href`: the ASCII box.** The label IS the payload — a one-time
|
|
14
|
+
* code — so there is nothing to click and a button would need a fabricated
|
|
15
|
+
* destination. Here email's `>> LABEL <<` box is exactly right, drawn by
|
|
16
|
+
* `../ascii` so the two channels cannot drift a column apart, and set in
|
|
17
|
+
* `rich_text_preformatted` so Slack renders it monospaced and cleanly
|
|
18
|
+
* selectable.
|
|
19
|
+
*
|
|
20
|
+
* That split is the whole principle in one element: same semantics, same brand
|
|
21
|
+
* drawing where the channel has nothing better, native primitive where it does.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { ActionsBlock, Button, KnownBlock } from "@slack/types";
|
|
25
|
+
|
|
26
|
+
import type { CallToAction } from "../../content";
|
|
27
|
+
import { type AsciiGeometry, renderCtaAscii } from "../ascii";
|
|
28
|
+
|
|
29
|
+
import { ACTIONS_MAX_ELEMENTS, chunk, plainText, preformatted } from "./blocks";
|
|
30
|
+
|
|
31
|
+
/** A linked CTA as one button. `plain_text` is literal — never escaped. */
|
|
32
|
+
export function ctaButton(cta: CallToAction): Button {
|
|
33
|
+
return {
|
|
34
|
+
type: "button",
|
|
35
|
+
text: plainText(cta.label),
|
|
36
|
+
// Only ever called for a linked CTA; the caller owns that branch.
|
|
37
|
+
url: cta.href,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* One `actions` block per run of linked CTAs, split at Slack's element cap.
|
|
43
|
+
*
|
|
44
|
+
* Adjacent CTAs coalesce rather than each claiming their own block: two buttons
|
|
45
|
+
* in one `actions` sit side by side the way a reader expects a choice to be
|
|
46
|
+
* offered, while two `actions` blocks stack them and read as two unrelated
|
|
47
|
+
* prompts. Slack caps a block at 25 elements, so a longer run splits rather
|
|
48
|
+
* than being rejected.
|
|
49
|
+
*/
|
|
50
|
+
export function ctaActions(ctas: CallToAction[]): ActionsBlock[] {
|
|
51
|
+
return chunk(ctas, ACTIONS_MAX_ELEMENTS).map((group) => ({
|
|
52
|
+
type: "actions",
|
|
53
|
+
elements: group.map(ctaButton),
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A standalone `callToAction` element.
|
|
59
|
+
*
|
|
60
|
+
* The hrefless form draws the box; the linked form is a button.
|
|
61
|
+
*/
|
|
62
|
+
export function renderCtaBlocks(
|
|
63
|
+
cta: CallToAction,
|
|
64
|
+
geometry: AsciiGeometry,
|
|
65
|
+
): KnownBlock[] {
|
|
66
|
+
return cta.href
|
|
67
|
+
? ctaActions([cta])
|
|
68
|
+
: preformatted(renderCtaAscii(cta, geometry));
|
|
69
|
+
}
|