@atelic-action/ui 0.7.1 → 0.8.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 +19 -2
- package/package.json +1 -1
- package/src/components/Callout.tsx +60 -0
- package/src/components/Checklist.tsx +43 -0
- package/src/components/Collapsible.tsx +43 -0
- package/src/components/ColumnChart.tsx +97 -0
- package/src/components/DataTable.tsx +194 -0
- package/src/components/StackedBar.tsx +46 -0
- package/src/components/StatRow.tsx +96 -0
- package/src/components/ThresholdScale.tsx +89 -0
- package/src/components/index.ts +33 -0
- package/src/components/tone.ts +7 -0
- package/src/email/atoms.tsx +26 -2
- package/src/email/scoreboard.tsx +28 -5
- package/src/styles/components.css +585 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { Tone } from "./tone";
|
|
2
|
+
|
|
3
|
+
export interface ThresholdZone {
|
|
4
|
+
label: string;
|
|
5
|
+
/** Where the zone ends, in the reading's unit; the last zone runs to `max`. */
|
|
6
|
+
upTo?: number;
|
|
7
|
+
tone: Tone;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ThresholdReading {
|
|
11
|
+
label: string;
|
|
12
|
+
value: number;
|
|
13
|
+
/** The figure as printed, e.g. "5.9 s". */
|
|
14
|
+
display: string;
|
|
15
|
+
tone: Tone;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ThresholdScaleProps {
|
|
19
|
+
/** The far end of the scale, in the reading's unit. */
|
|
20
|
+
max: number;
|
|
21
|
+
/** Zones in order, e.g. good, slow, poor. */
|
|
22
|
+
zones: ThresholdZone[];
|
|
23
|
+
/** The lines between zones, labeled, e.g. "2.5 s" at 2.5. */
|
|
24
|
+
ticks?: Array<{ at: number; label: string }>;
|
|
25
|
+
/** One line a reading, each a pin on the scale. */
|
|
26
|
+
readings: ThresholdReading[];
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A scale with zones and one line a reading: where a number sits against
|
|
32
|
+
* the lines that decide good, slow, and poor.
|
|
33
|
+
*/
|
|
34
|
+
export function ThresholdScale({ max, zones, ticks, readings, className }: ThresholdScaleProps) {
|
|
35
|
+
let from = 0;
|
|
36
|
+
const sized = zones.map((zone) => {
|
|
37
|
+
const to = zone.upTo ?? max;
|
|
38
|
+
const width = ((to - from) / max) * 100;
|
|
39
|
+
from = to;
|
|
40
|
+
return { ...zone, width };
|
|
41
|
+
});
|
|
42
|
+
return (
|
|
43
|
+
<div className={["threshold-scale", className].filter(Boolean).join(" ")}>
|
|
44
|
+
{readings.map((reading) => (
|
|
45
|
+
<div className="threshold-scale-row" key={reading.label}>
|
|
46
|
+
<span className="threshold-scale-label">{reading.label}</span>
|
|
47
|
+
<span
|
|
48
|
+
className="threshold-scale-track"
|
|
49
|
+
role="img"
|
|
50
|
+
aria-label={`${reading.label}: ${reading.display}`}
|
|
51
|
+
>
|
|
52
|
+
{sized.map((zone) => (
|
|
53
|
+
<span
|
|
54
|
+
className="threshold-scale-zone"
|
|
55
|
+
key={zone.label}
|
|
56
|
+
data-tone={zone.tone}
|
|
57
|
+
style={{ width: `${zone.width}%` }}
|
|
58
|
+
/>
|
|
59
|
+
))}
|
|
60
|
+
<span
|
|
61
|
+
className="threshold-scale-pin"
|
|
62
|
+
data-tone={reading.tone}
|
|
63
|
+
style={{ left: `${Math.min(reading.value / max, 1) * 100}%` }}
|
|
64
|
+
/>
|
|
65
|
+
</span>
|
|
66
|
+
<b className="threshold-scale-value" data-tone={reading.tone}>
|
|
67
|
+
{reading.display}
|
|
68
|
+
</b>
|
|
69
|
+
</div>
|
|
70
|
+
))}
|
|
71
|
+
<div className="threshold-scale-row is-axis" aria-hidden="true">
|
|
72
|
+
<span className="threshold-scale-label" />
|
|
73
|
+
<span className="threshold-scale-axis">
|
|
74
|
+
{sized.map((zone) => (
|
|
75
|
+
<span key={zone.label} data-tone={zone.tone} style={{ width: `${zone.width}%` }}>
|
|
76
|
+
{zone.label}
|
|
77
|
+
</span>
|
|
78
|
+
))}
|
|
79
|
+
{ticks?.map((tick) => (
|
|
80
|
+
<i key={tick.label} style={{ left: `${(tick.at / max) * 100}%` }}>
|
|
81
|
+
{tick.label}
|
|
82
|
+
</i>
|
|
83
|
+
))}
|
|
84
|
+
</span>
|
|
85
|
+
<span className="threshold-scale-value" />
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
package/src/components/index.ts
CHANGED
|
@@ -1 +1,34 @@
|
|
|
1
|
+
export { Callout, type CalloutProps } from "./Callout";
|
|
2
|
+
export { Checklist, type ChecklistItem, type ChecklistProps } from "./Checklist";
|
|
3
|
+
export { Collapsible, type CollapsibleProps } from "./Collapsible";
|
|
4
|
+
export {
|
|
5
|
+
ColumnChart,
|
|
6
|
+
type ColumnChartBar,
|
|
7
|
+
type ColumnChartGroup,
|
|
8
|
+
type ColumnChartProps,
|
|
9
|
+
} from "./ColumnChart";
|
|
10
|
+
export {
|
|
11
|
+
DataTable,
|
|
12
|
+
type DataTableCell,
|
|
13
|
+
type DataTableColumn,
|
|
14
|
+
type DataTableProps,
|
|
15
|
+
type DataTableRow,
|
|
16
|
+
type DataTableValueCell,
|
|
17
|
+
type SortDirection,
|
|
18
|
+
} from "./DataTable";
|
|
1
19
|
export { NotFound, type NotFoundProps } from "./NotFound";
|
|
20
|
+
export { StackedBar, type StackedBarProps, type StackedBarSegment } from "./StackedBar";
|
|
21
|
+
export {
|
|
22
|
+
type StatDelta,
|
|
23
|
+
type StatDirection,
|
|
24
|
+
type StatItem,
|
|
25
|
+
StatRow,
|
|
26
|
+
type StatRowProps,
|
|
27
|
+
} from "./StatRow";
|
|
28
|
+
export {
|
|
29
|
+
type ThresholdReading,
|
|
30
|
+
ThresholdScale,
|
|
31
|
+
type ThresholdScaleProps,
|
|
32
|
+
type ThresholdZone,
|
|
33
|
+
} from "./ThresholdScale";
|
|
34
|
+
export type { Tone } from "./tone";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a figure, a mark, or a bar reads: good, a warning, broken, plain
|
|
3
|
+
* information, or quiet. A component sets it as `data-tone`, and
|
|
4
|
+
* `styles/components.css` turns it into `--tone`, `--tone-fill`, and
|
|
5
|
+
* `--tone-bg` for whatever sits inside.
|
|
6
|
+
*/
|
|
7
|
+
export type Tone = "good" | "warn" | "bad" | "info" | "muted";
|
package/src/email/atoms.tsx
CHANGED
|
@@ -14,10 +14,12 @@ export type EyebrowProps = {
|
|
|
14
14
|
text: string;
|
|
15
15
|
/** Reads as a heading on a phone: larger, bold, in ink rather than faint. */
|
|
16
16
|
strong?: boolean;
|
|
17
|
+
/** Something small set right on the same line: a legend, a count. */
|
|
18
|
+
aside?: ReactNode;
|
|
17
19
|
};
|
|
18
20
|
|
|
19
21
|
/** A section label between cards. */
|
|
20
|
-
export function Eyebrow({ text, strong = false }: EyebrowProps) {
|
|
22
|
+
export function Eyebrow({ text, strong = false, aside }: EyebrowProps) {
|
|
21
23
|
const { palette, fonts } = useEmailTheme();
|
|
22
24
|
const look = strong
|
|
23
25
|
? {
|
|
@@ -28,9 +30,31 @@ export function Eyebrow({ text, strong = false }: EyebrowProps) {
|
|
|
28
30
|
color: palette.ink,
|
|
29
31
|
}
|
|
30
32
|
: { ...eyebrowStyle(fonts), color: palette.faint };
|
|
33
|
+
if (aside === undefined || aside === null) {
|
|
34
|
+
return (
|
|
35
|
+
<tr>
|
|
36
|
+
<td style={{ padding: "36px 8px 12px", ...look }}>{text}</td>
|
|
37
|
+
</tr>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
// With an aside (a legend, a count) the eyebrow keeps its cell and the
|
|
41
|
+
// aside sits right on the same line, bottom aligned to the text.
|
|
31
42
|
return (
|
|
32
43
|
<tr>
|
|
33
|
-
<td style={{ padding: "36px 8px 12px"
|
|
44
|
+
<td style={{ padding: "36px 8px 12px" }}>
|
|
45
|
+
<table {...tableReset} width="100%">
|
|
46
|
+
<tbody>
|
|
47
|
+
<tr>
|
|
48
|
+
<td valign="bottom" style={look}>
|
|
49
|
+
{text}
|
|
50
|
+
</td>
|
|
51
|
+
<td valign="bottom" align="right" style={{ textAlign: "right" }}>
|
|
52
|
+
{aside}
|
|
53
|
+
</td>
|
|
54
|
+
</tr>
|
|
55
|
+
</tbody>
|
|
56
|
+
</table>
|
|
57
|
+
</td>
|
|
34
58
|
</tr>
|
|
35
59
|
);
|
|
36
60
|
}
|
package/src/email/scoreboard.tsx
CHANGED
|
@@ -473,7 +473,7 @@ export type RecordTimelineStripProps = { timeline: RecordTimeline };
|
|
|
473
473
|
* (Company Cards IA, 2026-09-29).
|
|
474
474
|
*/
|
|
475
475
|
export function RecordTimelineStrip({ timeline }: RecordTimelineStripProps) {
|
|
476
|
-
const { palette } = useEmailTheme();
|
|
476
|
+
const { palette, fonts } = useEmailTheme();
|
|
477
477
|
const days = Math.max(1, Math.round(timeline.days));
|
|
478
478
|
const touch = new Set(timeline.touches.map((d) => Math.round(d)));
|
|
479
479
|
const open = new Set(timeline.opens.map((d) => Math.round(d)));
|
|
@@ -486,26 +486,37 @@ export function RecordTimelineStrip({ timeline }: RecordTimelineStripProps) {
|
|
|
486
486
|
else cells.push({ ago, span: 1, sent, opened });
|
|
487
487
|
}
|
|
488
488
|
const unit = 100 / days;
|
|
489
|
+
// The axis under the strip: how far back it reaches on the left, today on
|
|
490
|
+
// the right (Forni, 2026-09-29, asking whether it was a thirty day strip).
|
|
491
|
+
const axisStyle: CSSProperties = {
|
|
492
|
+
fontFamily: fonts.mono,
|
|
493
|
+
fontSize: "9px",
|
|
494
|
+
lineHeight: "12px",
|
|
495
|
+
color: palette.faint,
|
|
496
|
+
paddingTop: "2px",
|
|
497
|
+
};
|
|
489
498
|
return (
|
|
490
499
|
<table
|
|
491
500
|
{...tableReset}
|
|
492
501
|
width="100%"
|
|
493
502
|
style={{
|
|
494
503
|
marginTop: "8px",
|
|
495
|
-
height: "14px",
|
|
496
504
|
textAlign: "center",
|
|
497
|
-
borderBottom: `1px solid ${palette.line}`,
|
|
498
|
-
borderRight: `1.5px solid ${palette.ink}`,
|
|
499
505
|
}}
|
|
500
506
|
>
|
|
501
507
|
<tbody>
|
|
502
508
|
<tr>
|
|
503
|
-
{cells.map((cell) => (
|
|
509
|
+
{cells.map((cell, i) => (
|
|
504
510
|
<td
|
|
505
511
|
key={cell.ago}
|
|
506
512
|
valign="bottom"
|
|
507
513
|
colSpan={cell.span}
|
|
508
514
|
width={`${Math.round(unit * cell.span * 100) / 100}%`}
|
|
515
|
+
height="14"
|
|
516
|
+
style={{
|
|
517
|
+
borderBottom: `1px solid ${palette.line}`,
|
|
518
|
+
...(i === cells.length - 1 ? { borderRight: `1.5px solid ${palette.ink}` } : {}),
|
|
519
|
+
}}
|
|
509
520
|
>
|
|
510
521
|
{cell.sent ? (
|
|
511
522
|
<div
|
|
@@ -526,6 +537,18 @@ export function RecordTimelineStrip({ timeline }: RecordTimelineStripProps) {
|
|
|
526
537
|
</td>
|
|
527
538
|
))}
|
|
528
539
|
</tr>
|
|
540
|
+
<tr>
|
|
541
|
+
<td colSpan={Math.max(1, Math.ceil(days / 2))} align="left" style={axisStyle}>
|
|
542
|
+
{`${days}d`}
|
|
543
|
+
</td>
|
|
544
|
+
<td
|
|
545
|
+
colSpan={Math.max(1, Math.floor(days / 2))}
|
|
546
|
+
align="right"
|
|
547
|
+
style={{ ...axisStyle, textAlign: "right" }}
|
|
548
|
+
>
|
|
549
|
+
today
|
|
550
|
+
</td>
|
|
551
|
+
</tr>
|
|
529
552
|
</tbody>
|
|
530
553
|
</table>
|
|
531
554
|
);
|