@atelic-action/ui 0.4.0 → 0.5.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/README.md +22 -8
- package/package.json +1 -1
- package/src/email/atoms.tsx +16 -5
- package/src/email/frame.tsx +73 -47
- package/src/email/scoreboard.tsx +113 -27
- package/src/email/text.ts +15 -4
- package/src/tokens/palettes.ts +10 -0
package/README.md
CHANGED
|
@@ -149,18 +149,30 @@ const html = renderEmail({
|
|
|
149
149
|
|
|
150
150
|
Runner emails are mobile first. A layout that squashes on a phone is fixed here, in the components every runner shares, never in a report page. A fixed column table is for numbers alone and never carries a name; names go in a `RecordStack`.
|
|
151
151
|
|
|
152
|
-
`RecordStack` takes `records`, each a `RecordStackItem` (`title`, `url`, `meta`, `note`), and renders one table row per record: the title on its own line at 15px, bold and wrapping freely, linked when `url` is set; a mono meta line beneath it with the items joined by middle dots (empty items drop); an optional note beneath that; a hairline between records and none after the last. No cell carries a width, so a name of any length wraps at 320 pixels instead of squashing. `recordStackText(records)` is its plain text twin: each title on its own line under a two space indent, the meta and the note wrapped under a four space indent, a blank line between records, and no line past `textWidth` (68 columns, indent included).
|
|
152
|
+
`RecordStack` takes `records`, each a `RecordStackItem` (`title`, `url`, `meta`, `note`, `badge`, `callout`), and renders one table row per record: the title on its own line at 15px, bold and wrapping freely, linked when `url` is set; a mono meta line beneath it with the items joined by middle dots (empty items drop); an optional note beneath that; a hairline between records and none after the last. No cell carries a width, so a name of any length wraps at 320 pixels instead of squashing. An optional `badge` (a stage word such as `"Contacted"`) sits on the title line in a small mono uppercase pill on a `line` hairline, in its own right aligned unbroken cell, so the title keeps wrapping beside it and the badge stays on the title's first line. An optional `callout` (`{ eyebrow, text }`) sets a short task under the meta and the note in the accented `Note` idiom: the eyebrow in the accent, the text at 13px, and a 2px accent rule down its left edge. A record with neither renders exactly as it did before. `recordStackText(records)` is its plain text twin: each title on its own line under a two space indent with any badge after it in square brackets (`Blue Heron Plumbing [Contacted]`), the meta and the note wrapped under a four space indent, any callout as its eyebrow in upper case over its text under that same indent, a blank line between records, and no line past `textWidth` (68 columns, indent included).
|
|
153
153
|
|
|
154
|
-
`StatStrip` lays its stats out as inline block cells with an 88 pixel floor inside one centered cell, so six or seven stats flow onto a second row on a phone rather than shrinking. Each stat takes an optional `delta` (`"+3
|
|
154
|
+
`StatStrip` lays its stats out as inline block cells with an 88 pixel floor inside one centered cell, so six or seven stats flow onto a second row on a phone rather than shrinking. Each stat takes an optional `delta` (`"+3 (12%)"`), set small under its label and colored by its sign: one that opens on `+` wears the palette's `up`, one that opens on `-` its `down`, and anything else stays muted.
|
|
155
|
+
|
|
156
|
+
`Masthead` takes an optional `meta` (`"Week 39 · 09/21 to 09/27"`) set in the eyebrow style at the right end of the row, so the title card no longer carries the week. It is a right aligned, unbroken block floated after the wordmark and title: on the same line while the row has room, and dropped under the title on its own line, still right aligned, when it does not (checked at 320 pixels). `Eyebrow` takes `strong`, which sets the label at 15px and weight 700 in ink rather than faint, with a slightly tighter tracking, so a section such as `MQL · 15` reads as a heading on a phone.
|
|
155
157
|
|
|
156
158
|
```tsx
|
|
157
159
|
const records = [
|
|
158
|
-
{
|
|
160
|
+
{
|
|
161
|
+
title: "Pinewood Cabinetry",
|
|
162
|
+
url: "https://example.test",
|
|
163
|
+
meta: ["Lead", "Longmont", "fit 14"],
|
|
164
|
+
note: "Answered the audit inside a day.",
|
|
165
|
+
badge: "Contacted",
|
|
166
|
+
callout: { eyebrow: "Next", text: "Walk the Business Profile findings Thursday." },
|
|
167
|
+
},
|
|
159
168
|
];
|
|
160
169
|
|
|
170
|
+
<Masthead title="Pipeline" meta="Week 39 · 09/21 to 09/27" />;
|
|
171
|
+
<Eyebrow text="MQL · 15" strong />;
|
|
172
|
+
|
|
161
173
|
<Card>
|
|
162
174
|
<Row last={false}>
|
|
163
|
-
<StatStrip stats={[{ n: 6, label: "Lead", delta: "+3
|
|
175
|
+
<StatStrip stats={[{ n: 6, label: "Lead", delta: "+3 (12%)" }, { n: 2, label: "MQL", delta: "-1 (4%)" }]} />
|
|
164
176
|
</Row>
|
|
165
177
|
<Row last>
|
|
166
178
|
<RecordStack records={records} />
|
|
@@ -178,7 +190,7 @@ Where the jq takes a pre rendered html string (`$rows`, `$body_html`, `$cells_ht
|
|
|
178
190
|
|
|
179
191
|
| jq Function | Component | Props |
|
|
180
192
|
|---|---|---|
|
|
181
|
-
| `eyebrow` | `Eyebrow` | `text` |
|
|
193
|
+
| `eyebrow` | `Eyebrow` | `text`, `strong` (added 0.5.0, no jq counterpart) |
|
|
182
194
|
| `card` | `Card` | `children` |
|
|
183
195
|
| `fold` | `Fold` | `summary`, `children` |
|
|
184
196
|
| `big_fold` | `BigFold` | `summary`, `count` (a `ReactNode`, absent for the jq's `""`), `children` |
|
|
@@ -203,10 +215,10 @@ Where the jq takes a pre rendered html string (`$rows`, `$body_html`, `$cells_ht
|
|
|
203
215
|
| `sub_eyebrow` | `SubEyebrow` | `text` |
|
|
204
216
|
| `badge` | `Badge` | `letter` |
|
|
205
217
|
| `day_strip` | `DayStrip` | `days`, `last` |
|
|
206
|
-
| `stat_strip` | `StatStrip` | `stats`, each with an optional `delta` (the component wraps where the jq does not) |
|
|
207
|
-
| none | `RecordStack` | `records` (born here on 2026-09-24, with no jq counterpart) |
|
|
218
|
+
| `stat_strip` | `StatStrip` | `stats`, each with an optional `delta` colored by its sign (the component wraps where the jq does not) |
|
|
219
|
+
| none | `RecordStack` | `records`, each with an optional `badge` and `callout` (born here on 2026-09-24, with no jq counterpart) |
|
|
208
220
|
| `records` | `Records` | `columns`, `rows`; a cell's `html` is a `ReactNode` |
|
|
209
|
-
| `masthead` | `Masthead` | `title`, `wordmark` (defaults to `atelic`) |
|
|
221
|
+
| `masthead` | `Masthead` | `title`, `wordmark` (defaults to `atelic`), `meta` (added 0.5.0, no jq counterpart) |
|
|
210
222
|
| `title_card` | `TitleCard` | `eyebrowText`, `headlineLines`, `lede`, `stats` (the rows under the lede, in place of the jq's `$stats_html`) |
|
|
211
223
|
| `footer` | `Footer` | `meta` |
|
|
212
224
|
| `page` | `renderEmail` | `title`, `preheader`, `children`, `palette`, `fonts` |
|
|
@@ -220,6 +232,8 @@ The plain text alternative part ports as plain functions with no React anywhere
|
|
|
220
232
|
|
|
221
233
|
`@atelic-action/ui/tokens` carries the palette and the font stacks with no React import, so a build script or a plain text renderer can read them. `toThemeCSS(palette)` writes the palette out as the site token declarations (`--surface`, `--surface-dark`, `--card`, `--ink`, `--primary`), and `themeTokenMap` exposes which palette key each token takes.
|
|
222
234
|
|
|
235
|
+
The palette carries two optional keys beyond the jq's eight: `up` (`#2F7A4B`, a calm green) and `down` (`#B23A2E`, a calm red, kept well apart from the orange accent), the colors a `StatStrip` delta wears by its sign. Both read on the cream card. They are optional so a client palette written before 0.5.0 still compiles, and a palette without them borrows the Atelic values. Neither maps to a site token.
|
|
236
|
+
|
|
223
237
|
## Releasing
|
|
224
238
|
|
|
225
239
|
1. Bump `version` in `package.json` and merge it to `main`: patch for a fix, minor for a new component or prop, major for a breaking prop or class rename.
|
package/package.json
CHANGED
package/src/email/atoms.tsx
CHANGED
|
@@ -10,16 +10,27 @@ import { eyebrowStyle, tableReset, useEmailTheme } from "./theme";
|
|
|
10
10
|
* is a plain string and React does the escaping.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
export type EyebrowProps = {
|
|
13
|
+
export type EyebrowProps = {
|
|
14
|
+
text: string;
|
|
15
|
+
/** Reads as a heading on a phone: larger, bold, in ink rather than faint. */
|
|
16
|
+
strong?: boolean;
|
|
17
|
+
};
|
|
14
18
|
|
|
15
19
|
/** A section label between cards. */
|
|
16
|
-
export function Eyebrow({ text }: EyebrowProps) {
|
|
20
|
+
export function Eyebrow({ text, strong = false }: EyebrowProps) {
|
|
17
21
|
const { palette, fonts } = useEmailTheme();
|
|
22
|
+
const look = strong
|
|
23
|
+
? {
|
|
24
|
+
...eyebrowStyle(fonts),
|
|
25
|
+
fontSize: "15px",
|
|
26
|
+
fontWeight: "700",
|
|
27
|
+
letterSpacing: "0.06em",
|
|
28
|
+
color: palette.ink,
|
|
29
|
+
}
|
|
30
|
+
: { ...eyebrowStyle(fonts), color: palette.faint };
|
|
18
31
|
return (
|
|
19
32
|
<tr>
|
|
20
|
-
<td style={{ padding: "36px 8px 12px", ...
|
|
21
|
-
{text}
|
|
22
|
-
</td>
|
|
33
|
+
<td style={{ padding: "36px 8px 12px", ...look }}>{text}</td>
|
|
23
34
|
</tr>
|
|
24
35
|
);
|
|
25
36
|
}
|
package/src/email/frame.tsx
CHANGED
|
@@ -14,62 +14,88 @@ export type MastheadProps = {
|
|
|
14
14
|
title: string;
|
|
15
15
|
/** The wordmark at the top left. */
|
|
16
16
|
wordmark?: string;
|
|
17
|
+
/**
|
|
18
|
+
* A quiet fact at the right end of the row, such as the week. It sits on
|
|
19
|
+
* the title's line while it fits and drops under it, still right aligned,
|
|
20
|
+
* when the row runs out of room.
|
|
21
|
+
*/
|
|
22
|
+
meta?: string;
|
|
17
23
|
};
|
|
18
24
|
|
|
19
25
|
/**
|
|
20
26
|
* The wordmark and one orange rule at the top left, the email's title beside
|
|
21
|
-
* them: the runner's name, not the week.
|
|
27
|
+
* them: the runner's name, not the week. A `meta` rides at the right end of
|
|
28
|
+
* the row in its own right aligned, unbroken block, floated after the brand so
|
|
29
|
+
* that on a narrow screen it drops under the title instead of squashing it.
|
|
22
30
|
*/
|
|
23
|
-
export function Masthead({ title, wordmark = "atelic" }: MastheadProps) {
|
|
31
|
+
export function Masthead({ title, wordmark = "atelic", meta }: MastheadProps) {
|
|
24
32
|
const { palette, fonts } = useEmailTheme();
|
|
33
|
+
const brand = (
|
|
34
|
+
<table {...tableReset}>
|
|
35
|
+
<tbody>
|
|
36
|
+
<tr>
|
|
37
|
+
<td
|
|
38
|
+
style={{
|
|
39
|
+
fontSize: "20px",
|
|
40
|
+
fontWeight: "600",
|
|
41
|
+
letterSpacing: "-0.045em",
|
|
42
|
+
color: palette.ink,
|
|
43
|
+
paddingRight: "8px",
|
|
44
|
+
lineHeight: "1",
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
47
|
+
{wordmark}
|
|
48
|
+
</td>
|
|
49
|
+
<td width="34" style={{ width: "34px", verticalAlign: "middle", paddingRight: "12px" }}>
|
|
50
|
+
<div
|
|
51
|
+
style={{
|
|
52
|
+
width: "34px",
|
|
53
|
+
height: "2px",
|
|
54
|
+
fontSize: "0",
|
|
55
|
+
lineHeight: "0",
|
|
56
|
+
backgroundColor: palette.accent,
|
|
57
|
+
background: `linear-gradient(90deg,${palette.accent},${fadeStop(palette.accent)})`,
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
{"\u00a0"}
|
|
61
|
+
</div>
|
|
62
|
+
</td>
|
|
63
|
+
<td
|
|
64
|
+
style={{
|
|
65
|
+
...eyebrowStyle(fonts),
|
|
66
|
+
color: palette.faint,
|
|
67
|
+
verticalAlign: "middle",
|
|
68
|
+
lineHeight: "1",
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
{title}
|
|
72
|
+
</td>
|
|
73
|
+
</tr>
|
|
74
|
+
</tbody>
|
|
75
|
+
</table>
|
|
76
|
+
);
|
|
25
77
|
return (
|
|
26
78
|
<tr>
|
|
27
79
|
<td style={{ padding: "8px 8px 22px", fontFamily: fonts.sans }}>
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
<div
|
|
48
|
-
style={{
|
|
49
|
-
width: "34px",
|
|
50
|
-
height: "2px",
|
|
51
|
-
fontSize: "0",
|
|
52
|
-
lineHeight: "0",
|
|
53
|
-
backgroundColor: palette.accent,
|
|
54
|
-
background: `linear-gradient(90deg,${palette.accent},${fadeStop(palette.accent)})`,
|
|
55
|
-
}}
|
|
56
|
-
>
|
|
57
|
-
{"\u00a0"}
|
|
58
|
-
</div>
|
|
59
|
-
</td>
|
|
60
|
-
<td
|
|
61
|
-
style={{
|
|
62
|
-
...eyebrowStyle(fonts),
|
|
63
|
-
color: palette.faint,
|
|
64
|
-
verticalAlign: "middle",
|
|
65
|
-
lineHeight: "1",
|
|
66
|
-
}}
|
|
67
|
-
>
|
|
68
|
-
{title}
|
|
69
|
-
</td>
|
|
70
|
-
</tr>
|
|
71
|
-
</tbody>
|
|
72
|
-
</table>
|
|
80
|
+
{meta ? (
|
|
81
|
+
<>
|
|
82
|
+
<div style={{ display: "inline-block", verticalAlign: "middle" }}>{brand}</div>
|
|
83
|
+
<div
|
|
84
|
+
style={{
|
|
85
|
+
float: "right",
|
|
86
|
+
textAlign: "right",
|
|
87
|
+
whiteSpace: "nowrap",
|
|
88
|
+
...eyebrowStyle(fonts),
|
|
89
|
+
lineHeight: "20px",
|
|
90
|
+
color: palette.faint,
|
|
91
|
+
}}
|
|
92
|
+
>
|
|
93
|
+
{meta}
|
|
94
|
+
</div>
|
|
95
|
+
</>
|
|
96
|
+
) : (
|
|
97
|
+
brand
|
|
98
|
+
)}
|
|
73
99
|
</td>
|
|
74
100
|
</tr>
|
|
75
101
|
);
|
package/src/email/scoreboard.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Fragment, type ReactNode } from "react";
|
|
1
|
+
import { type CSSProperties, Fragment, type ReactNode } from "react";
|
|
2
|
+
import { atelicPalette, type Palette } from "../tokens";
|
|
2
3
|
import type { RecordStackItem } from "./text";
|
|
3
4
|
import { eyebrowStyle, tableReset, useEmailTheme } from "./theme";
|
|
4
5
|
|
|
@@ -327,9 +328,22 @@ export function DayStrip({ days, last }: DayStripProps) {
|
|
|
327
328
|
export type StatStripEntry = {
|
|
328
329
|
n: string | number;
|
|
329
330
|
label: string;
|
|
330
|
-
/**
|
|
331
|
+
/**
|
|
332
|
+
* A change beside the number, small under the label: "+3 (12%)". One that
|
|
333
|
+
* opens on "+" wears the palette's `up`, one that opens on "-" its `down`,
|
|
334
|
+
* anything else stays muted.
|
|
335
|
+
*/
|
|
331
336
|
delta?: string;
|
|
332
337
|
};
|
|
338
|
+
|
|
339
|
+
/** The color a delta wears, read off its sign. */
|
|
340
|
+
export function deltaColor(delta: string, palette: Palette): string {
|
|
341
|
+
if (delta.startsWith("+")) return palette.up ?? atelicPalette.up ?? palette.faint;
|
|
342
|
+
if (delta.startsWith("-") || delta.startsWith("\u2212")) {
|
|
343
|
+
return palette.down ?? atelicPalette.down ?? palette.faint;
|
|
344
|
+
}
|
|
345
|
+
return palette.faint;
|
|
346
|
+
}
|
|
333
347
|
export type StatStripProps = { stats: StatStripEntry[] };
|
|
334
348
|
|
|
335
349
|
/**
|
|
@@ -377,7 +391,11 @@ export function StatStrip({ stats }: StatStripProps) {
|
|
|
377
391
|
<>
|
|
378
392
|
<br />
|
|
379
393
|
<span
|
|
380
|
-
style={{
|
|
394
|
+
style={{
|
|
395
|
+
fontFamily: fonts.mono,
|
|
396
|
+
fontSize: "11px",
|
|
397
|
+
color: deltaColor(entry.delta, palette),
|
|
398
|
+
}}
|
|
381
399
|
>
|
|
382
400
|
{entry.delta}
|
|
383
401
|
</span>
|
|
@@ -392,6 +410,35 @@ export function StatStrip({ stats }: StatStripProps) {
|
|
|
392
410
|
);
|
|
393
411
|
}
|
|
394
412
|
|
|
413
|
+
type RecordBadgeProps = { text: string };
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* A word in a small mono pill on a hairline, the Badge idiom sized for a stage
|
|
417
|
+
* such as "Contacted". Only RecordStack wears it.
|
|
418
|
+
*/
|
|
419
|
+
function RecordBadge({ text }: RecordBadgeProps) {
|
|
420
|
+
const { palette, fonts } = useEmailTheme();
|
|
421
|
+
return (
|
|
422
|
+
<span
|
|
423
|
+
style={{
|
|
424
|
+
display: "inline-block",
|
|
425
|
+
fontFamily: fonts.mono,
|
|
426
|
+
fontSize: "10px",
|
|
427
|
+
lineHeight: "14px",
|
|
428
|
+
letterSpacing: "0.08em",
|
|
429
|
+
textTransform: "uppercase",
|
|
430
|
+
padding: "0 6px",
|
|
431
|
+
border: `1px solid ${palette.line}`,
|
|
432
|
+
borderRadius: "3px",
|
|
433
|
+
color: palette.dim,
|
|
434
|
+
whiteSpace: "nowrap",
|
|
435
|
+
}}
|
|
436
|
+
>
|
|
437
|
+
{text}
|
|
438
|
+
</span>
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
395
442
|
export type { RecordStackItem };
|
|
396
443
|
export type RecordStackProps = { records: RecordStackItem[] };
|
|
397
444
|
|
|
@@ -403,12 +450,33 @@ export type RecordStackProps = { records: RecordStackItem[] };
|
|
|
403
450
|
*/
|
|
404
451
|
export function RecordStack({ records }: RecordStackProps) {
|
|
405
452
|
const { palette, fonts } = useEmailTheme();
|
|
453
|
+
const titleStyle: CSSProperties = {
|
|
454
|
+
fontSize: "15px",
|
|
455
|
+
fontWeight: "600",
|
|
456
|
+
lineHeight: "1.35",
|
|
457
|
+
color: palette.ink,
|
|
458
|
+
wordBreak: "break-word",
|
|
459
|
+
};
|
|
406
460
|
return (
|
|
407
461
|
<table {...tableReset} width="100%" style={{ fontFamily: fonts.sans, color: palette.ink }}>
|
|
408
462
|
<tbody>
|
|
409
463
|
{records.map((record, i) => {
|
|
410
464
|
const last = i === records.length - 1;
|
|
411
465
|
const meta = record.meta.filter((m) => m !== "");
|
|
466
|
+
const title = record.url ? (
|
|
467
|
+
<a
|
|
468
|
+
href={record.url}
|
|
469
|
+
style={{
|
|
470
|
+
color: palette.ink,
|
|
471
|
+
textDecoration: "none",
|
|
472
|
+
borderBottom: `1px solid ${palette.accent}`,
|
|
473
|
+
}}
|
|
474
|
+
>
|
|
475
|
+
{record.title}
|
|
476
|
+
</a>
|
|
477
|
+
) : (
|
|
478
|
+
record.title
|
|
479
|
+
);
|
|
412
480
|
return (
|
|
413
481
|
// biome-ignore lint/suspicious/noArrayIndexKey: a record's position is its identity
|
|
414
482
|
<tr key={i}>
|
|
@@ -419,30 +487,29 @@ export function RecordStack({ records }: RecordStackProps) {
|
|
|
419
487
|
...(last ? {} : { borderBottom: `1px solid ${palette.line}` }),
|
|
420
488
|
}}
|
|
421
489
|
>
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
</div>
|
|
490
|
+
{record.badge ? (
|
|
491
|
+
<table {...tableReset} width="100%">
|
|
492
|
+
<tbody>
|
|
493
|
+
<tr>
|
|
494
|
+
<td style={{ ...titleStyle, verticalAlign: "top" }}>{title}</td>
|
|
495
|
+
<td
|
|
496
|
+
align="right"
|
|
497
|
+
style={{
|
|
498
|
+
textAlign: "right",
|
|
499
|
+
whiteSpace: "nowrap",
|
|
500
|
+
verticalAlign: "top",
|
|
501
|
+
paddingLeft: "12px",
|
|
502
|
+
paddingTop: "2px",
|
|
503
|
+
}}
|
|
504
|
+
>
|
|
505
|
+
<RecordBadge text={record.badge} />
|
|
506
|
+
</td>
|
|
507
|
+
</tr>
|
|
508
|
+
</tbody>
|
|
509
|
+
</table>
|
|
510
|
+
) : (
|
|
511
|
+
<div style={titleStyle}>{title}</div>
|
|
512
|
+
)}
|
|
446
513
|
{meta.length > 0 ? (
|
|
447
514
|
<div
|
|
448
515
|
style={{
|
|
@@ -468,6 +535,25 @@ export function RecordStack({ records }: RecordStackProps) {
|
|
|
468
535
|
{record.note}
|
|
469
536
|
</div>
|
|
470
537
|
) : null}
|
|
538
|
+
{record.callout ? (
|
|
539
|
+
<div
|
|
540
|
+
style={{
|
|
541
|
+
marginTop: "10px",
|
|
542
|
+
paddingLeft: "10px",
|
|
543
|
+
borderLeft: `2px solid ${palette.accent}`,
|
|
544
|
+
fontFamily: fonts.sans,
|
|
545
|
+
fontSize: "13px",
|
|
546
|
+
lineHeight: "1.5",
|
|
547
|
+
color: palette.ink,
|
|
548
|
+
}}
|
|
549
|
+
>
|
|
550
|
+
<span style={{ ...eyebrowStyle(fonts), color: palette.accent }}>
|
|
551
|
+
{record.callout.eyebrow}
|
|
552
|
+
</span>
|
|
553
|
+
<br />
|
|
554
|
+
{record.callout.text}
|
|
555
|
+
</div>
|
|
556
|
+
) : null}
|
|
471
557
|
</td>
|
|
472
558
|
</tr>
|
|
473
559
|
);
|
package/src/email/text.ts
CHANGED
|
@@ -135,21 +135,32 @@ export type RecordStackItem = {
|
|
|
135
135
|
/** Facts joined by a middle dot on the line under the title; empty items drop. */
|
|
136
136
|
meta: string[];
|
|
137
137
|
note?: string;
|
|
138
|
+
/** A stage word on the title line, right aligned in a pill; "[Contacted]" in the text twin. */
|
|
139
|
+
badge?: string;
|
|
140
|
+
/** A short task under the record, an orange eyebrow over a line of text. */
|
|
141
|
+
callout?: { eyebrow: string; text: string };
|
|
138
142
|
};
|
|
139
143
|
|
|
140
144
|
/**
|
|
141
145
|
* The plain text twin of `RecordStack`: each title on its own line under a
|
|
142
|
-
* two space indent
|
|
143
|
-
*
|
|
144
|
-
*
|
|
146
|
+
* two space indent with any badge after it in square brackets, the meta joined
|
|
147
|
+
* by middle dots and the note beneath it, both wrapped under a four space
|
|
148
|
+
* indent, then any callout as its eyebrow in upper case over its text under
|
|
149
|
+
* the same indent, a blank line between records. No line runs past
|
|
150
|
+
* `textWidth`.
|
|
145
151
|
*/
|
|
146
152
|
export function recordStackText(records: RecordStackItem[]): string {
|
|
147
153
|
return records
|
|
148
154
|
.map((record) => {
|
|
149
|
-
const
|
|
155
|
+
const title = record.badge ? `${record.title} [${record.badge}]` : record.title;
|
|
156
|
+
const lines = [wrapIndented(title, 2)];
|
|
150
157
|
const meta = record.meta.filter((m) => m !== "");
|
|
151
158
|
if (meta.length > 0) lines.push(wrapIndented(meta.join(" · "), 4));
|
|
152
159
|
if (record.note) lines.push(wrapIndented(record.note, 4));
|
|
160
|
+
if (record.callout) {
|
|
161
|
+
lines.push(wrapIndented(asciiUpcase(record.callout.eyebrow), 4));
|
|
162
|
+
lines.push(wrapIndented(record.callout.text, 4));
|
|
163
|
+
}
|
|
153
164
|
return lines.join("\n");
|
|
154
165
|
})
|
|
155
166
|
.join("\n\n");
|
package/src/tokens/palettes.ts
CHANGED
|
@@ -24,6 +24,14 @@ export type Palette = {
|
|
|
24
24
|
line: string;
|
|
25
25
|
/** The lighter rule inside a card. */
|
|
26
26
|
hair: string;
|
|
27
|
+
/**
|
|
28
|
+
* A change for the better, such as a positive delta under a stat. Optional
|
|
29
|
+
* so a palette written before 0.5.0 still compiles; a palette without it
|
|
30
|
+
* borrows the Atelic value.
|
|
31
|
+
*/
|
|
32
|
+
up?: string;
|
|
33
|
+
/** A change for the worse, the negative delta's color. Optional the same way. */
|
|
34
|
+
down?: string;
|
|
27
35
|
};
|
|
28
36
|
|
|
29
37
|
export const atelicPalette: Palette = {
|
|
@@ -35,6 +43,8 @@ export const atelicPalette: Palette = {
|
|
|
35
43
|
accent: "#FC4A1A",
|
|
36
44
|
line: "#E6DFD2",
|
|
37
45
|
hair: "#F0EAE0",
|
|
46
|
+
up: "#2F7A4B",
|
|
47
|
+
down: "#B23A2E",
|
|
38
48
|
};
|
|
39
49
|
|
|
40
50
|
/**
|