@atelic-action/artifact 1.3.0 → 3.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/README.md +6 -5
- package/package.json +3 -3
- package/src/documents/Read.tsx +203 -302
- package/src/documents/Writeup.tsx +93 -72
- package/src/documents/documents.css +104 -415
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
+
type ArtifactParagraph,
|
|
2
3
|
GradeChip,
|
|
3
4
|
gradeLabel,
|
|
4
5
|
newTabProps,
|
|
6
|
+
Runs,
|
|
5
7
|
Shot,
|
|
6
8
|
SignOff,
|
|
7
9
|
type WriteupGrade,
|
|
@@ -16,6 +18,7 @@ import {
|
|
|
16
18
|
type ColumnChartGroup,
|
|
17
19
|
DataTable,
|
|
18
20
|
Disclosure,
|
|
21
|
+
ReportCard,
|
|
19
22
|
StackedBar,
|
|
20
23
|
type StackedBarSegment,
|
|
21
24
|
type StatItem,
|
|
@@ -25,7 +28,7 @@ import {
|
|
|
25
28
|
type ThresholdZone,
|
|
26
29
|
type Tone,
|
|
27
30
|
} from "@atelic-action/ui/components";
|
|
28
|
-
import { ArrowUpRight,
|
|
31
|
+
import { ArrowUpRight, CircleAlert, CircleCheck, CircleX, Star } from "lucide-react";
|
|
29
32
|
import type { ReactNode } from "react";
|
|
30
33
|
|
|
31
34
|
export type { WriteupGrade, WriteupPerson, WriteupShot } from "@atelic-action/ui/artifact";
|
|
@@ -52,8 +55,8 @@ export type WriteupTone = Tone;
|
|
|
52
55
|
export interface WriteupBullet {
|
|
53
56
|
/** The point, in a full sentence. The icon sits beside it. */
|
|
54
57
|
lead: string;
|
|
55
|
-
/** The one explainer that says why it matters. */
|
|
56
|
-
body?: ReactNode;
|
|
58
|
+
/** The one explainer that says why it matters: a node, or a paragraph of runs when the content arrives as JSON. */
|
|
59
|
+
body?: ReactNode | ArtifactParagraph;
|
|
57
60
|
/** A win the owner already has: marked good. Everything else reads as needing work. */
|
|
58
61
|
win?: boolean;
|
|
59
62
|
/** Overrides the tone, e.g. "info" for something not yet read. */
|
|
@@ -113,6 +116,8 @@ export interface WriteupTable extends BlockBase {
|
|
|
113
116
|
sortable?: boolean;
|
|
114
117
|
/** The direction a first click sorts in. */
|
|
115
118
|
firstSort?: "asc" | "desc";
|
|
119
|
+
/** The column hugs its content, for a swatch or a mark. */
|
|
120
|
+
fit?: boolean;
|
|
116
121
|
}>;
|
|
117
122
|
rows: Array<{ cells: WriteupCell[]; tone?: WriteupTone }>;
|
|
118
123
|
/** The sort the table opens in. */
|
|
@@ -199,8 +204,12 @@ export interface WriteupTopic {
|
|
|
199
204
|
oneLiner: string;
|
|
200
205
|
/** The section's grade, beside the heading. */
|
|
201
206
|
grade?: WriteupGrade;
|
|
202
|
-
/** Why it earned that letter: the rule it met, in a sentence. */
|
|
207
|
+
/** Why it earned that letter: the rule it met, in a sentence. The read sets its summary here. */
|
|
203
208
|
why?: ReactNode;
|
|
209
|
+
/** The word before the grade on the why line; defaults to "Why". */
|
|
210
|
+
whyLabel?: string;
|
|
211
|
+
/** What a screen reader hears before the grade's letter; defaults to "Grade". */
|
|
212
|
+
gradeWord?: string;
|
|
204
213
|
/** The section's blocks, in order. */
|
|
205
214
|
blocks?: WriteupTopicBlock[];
|
|
206
215
|
/** Findings without blocks: rendered as one findings block with the shots beside it. */
|
|
@@ -388,21 +397,43 @@ function Table({ block }: { block: WriteupTable }) {
|
|
|
388
397
|
);
|
|
389
398
|
}
|
|
390
399
|
|
|
400
|
+
/** A string or runs of text and links, the shape JSON content arrives in; a node of elements is anything else. */
|
|
401
|
+
function isRuns(value: unknown): value is ArtifactParagraph {
|
|
402
|
+
if (typeof value === "string") return true;
|
|
403
|
+
return (
|
|
404
|
+
Array.isArray(value) &&
|
|
405
|
+
value.every(
|
|
406
|
+
(run) =>
|
|
407
|
+
typeof run === "string" ||
|
|
408
|
+
(typeof run === "object" && run !== null && "text" in run && "href" in run),
|
|
409
|
+
)
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** A finding's body as a node, whichever way the content wrote it. */
|
|
414
|
+
function bodyNode(body: ReactNode | ArtifactParagraph | undefined): ReactNode {
|
|
415
|
+
if (body === undefined || body === null) return undefined;
|
|
416
|
+
return isRuns(body) ? <Runs paragraph={body} /> : body;
|
|
417
|
+
}
|
|
418
|
+
|
|
391
419
|
function Findings({ block, brief }: { block: WriteupFindings; brief?: string }) {
|
|
392
420
|
const hasShots = Boolean(block.shot || brief);
|
|
393
421
|
return (
|
|
394
422
|
<div className={`op-block op-findings${hasShots ? " has-shots" : ""}`}>
|
|
395
423
|
<div className="op-findings-list">
|
|
396
|
-
{block.items.map((item) =>
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
424
|
+
{block.items.map((item, i) => {
|
|
425
|
+
const shape = {
|
|
426
|
+
tone: item.tone ?? (item.win ? "good" : "warn"),
|
|
427
|
+
title: item.lead,
|
|
428
|
+
variant: item.box ? ("box" as const) : ("plain" as const),
|
|
429
|
+
};
|
|
430
|
+
return (
|
|
431
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: findings are static content in a fixed order, and a lead may repeat
|
|
432
|
+
<Callout key={i} {...shape}>
|
|
433
|
+
{bodyNode(item.body)}
|
|
434
|
+
</Callout>
|
|
435
|
+
);
|
|
436
|
+
})}
|
|
406
437
|
</div>
|
|
407
438
|
{hasShots && (
|
|
408
439
|
<div className="op-shots">
|
|
@@ -495,7 +526,14 @@ function TopicBlock({ block }: { block: WriteupTopicBlock }) {
|
|
|
495
526
|
}
|
|
496
527
|
}
|
|
497
528
|
|
|
498
|
-
|
|
529
|
+
/**
|
|
530
|
+
* The section card: the trade name linked to its definition, the one liner,
|
|
531
|
+
* the why line, the grade, the blocks, and the sources at the foot. The read
|
|
532
|
+
* renders its sections through it too, handing anything of its own (today and
|
|
533
|
+
* after, what we recommend) as `extra`, set after the blocks and before the
|
|
534
|
+
* sources, so the two documents share one layout.
|
|
535
|
+
*/
|
|
536
|
+
export function Topic({ topic, extra }: { topic: WriteupTopic; extra?: ReactNode }) {
|
|
499
537
|
const legacy: WriteupFindings | null = topic.bullets
|
|
500
538
|
? { kind: "findings", items: topic.bullets, shot: topic.shot, shot2: topic.shot2 }
|
|
501
539
|
: null;
|
|
@@ -519,13 +557,25 @@ function Topic({ topic }: { topic: WriteupTopic }) {
|
|
|
519
557
|
)}
|
|
520
558
|
</h3>
|
|
521
559
|
<p className="op-def">{topic.oneLiner}</p>
|
|
522
|
-
{topic.
|
|
560
|
+
{topic.why && (
|
|
523
561
|
<p className="op-why">
|
|
524
|
-
<b>
|
|
562
|
+
<b>
|
|
563
|
+
{topic.grade
|
|
564
|
+
? `${topic.whyLabel ?? "Why"} ${gradeLabel[topic.grade]}:`
|
|
565
|
+
: `${topic.whyLabel ?? "Why"}:`}
|
|
566
|
+
</b>{" "}
|
|
567
|
+
{topic.why}
|
|
525
568
|
</p>
|
|
526
569
|
)}
|
|
527
570
|
</div>
|
|
528
|
-
{topic.grade &&
|
|
571
|
+
{topic.grade && (
|
|
572
|
+
<>
|
|
573
|
+
<span className="op-sr-only">{`${topic.gradeWord ?? "Grade"} ${gradeLabel[topic.grade]}`}</span>
|
|
574
|
+
<span aria-hidden="true">
|
|
575
|
+
<GradeChip grade={topic.grade} />
|
|
576
|
+
</span>
|
|
577
|
+
</>
|
|
578
|
+
)}
|
|
529
579
|
</header>
|
|
530
580
|
<div className="op-blocks">
|
|
531
581
|
{topic.blocks?.map((block, i) => (
|
|
@@ -533,13 +583,15 @@ function Topic({ topic }: { topic: WriteupTopic }) {
|
|
|
533
583
|
<TopicBlock block={block} key={i} />
|
|
534
584
|
))}
|
|
535
585
|
{legacy && <Findings block={legacy} brief={brief} />}
|
|
586
|
+
{extra}
|
|
536
587
|
</div>
|
|
537
588
|
{topic.sources && topic.sources.length > 0 && (
|
|
538
589
|
<footer className="op-sources">
|
|
539
590
|
<span className="op-sources-label">{topic.sourcesLabel ?? "Sources"}</span>
|
|
540
591
|
<ul>
|
|
541
|
-
{topic.sources.map((source) => (
|
|
542
|
-
|
|
592
|
+
{topic.sources.map((source, i) => (
|
|
593
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: sources are static content in a fixed order, and a label may repeat
|
|
594
|
+
<li key={i}>
|
|
543
595
|
{source.href ? (
|
|
544
596
|
<a href={source.href} {...newTabProps(source.href, true)}>
|
|
545
597
|
{source.label}
|
|
@@ -558,61 +610,30 @@ function Topic({ topic }: { topic: WriteupTopic }) {
|
|
|
558
610
|
}
|
|
559
611
|
|
|
560
612
|
function Summary({ summary }: { summary: WriteupSummary }) {
|
|
561
|
-
const
|
|
613
|
+
const groups: Array<{ title: string; rows: WriteupSummaryRow[] }> = [];
|
|
562
614
|
for (const row of summary.rows) {
|
|
563
|
-
const last =
|
|
564
|
-
if (last && last.
|
|
565
|
-
else
|
|
615
|
+
const last = groups[groups.length - 1];
|
|
616
|
+
if (last && last.title === row.tier) last.rows.push(row);
|
|
617
|
+
else groups.push({ title: row.tier, rows: [row] });
|
|
566
618
|
}
|
|
567
|
-
const beforeLabel = summary.beforeLabel ?? "Before";
|
|
568
619
|
return (
|
|
569
|
-
<
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
<div className="op-ov-tier-label">{tier}</div>
|
|
587
|
-
<ul>
|
|
588
|
-
{rows.map((row) => (
|
|
589
|
-
<li className="op-ov-row" key={row.section}>
|
|
590
|
-
<span className="op-ov-grade">
|
|
591
|
-
{row.before && (
|
|
592
|
-
<span className="op-ov-before">
|
|
593
|
-
<span className="op-sr-only">{`${beforeLabel}: ${gradeLabel[row.before]}. `}</span>
|
|
594
|
-
<span aria-hidden="true">
|
|
595
|
-
<GradeChip grade={row.before} />
|
|
596
|
-
</span>
|
|
597
|
-
</span>
|
|
598
|
-
)}
|
|
599
|
-
<span className="op-sr-only">{`Grade ${gradeLabel[row.grade]}: `}</span>
|
|
600
|
-
<span aria-hidden="true">
|
|
601
|
-
<GradeChip grade={row.grade} />
|
|
602
|
-
</span>
|
|
603
|
-
</span>
|
|
604
|
-
<b className="op-ov-name">
|
|
605
|
-
{row.href ? <a href={row.href}>{row.section}</a> : row.section}
|
|
606
|
-
</b>
|
|
607
|
-
<span className="op-ov-read">{row.read}</span>
|
|
608
|
-
{row.href && <ChevronRight className="op-ov-go" size={18} aria-hidden="true" />}
|
|
609
|
-
</li>
|
|
610
|
-
))}
|
|
611
|
-
</ul>
|
|
612
|
-
</div>
|
|
613
|
-
))}
|
|
614
|
-
</div>
|
|
615
|
-
</section>
|
|
620
|
+
<ReportCard
|
|
621
|
+
className="op-ov"
|
|
622
|
+
id={summary.id}
|
|
623
|
+
title={summary.title}
|
|
624
|
+
legend={summary.legend}
|
|
625
|
+
labels={{ before: summary.beforeLabel }}
|
|
626
|
+
groups={groups.map((group) => ({
|
|
627
|
+
title: group.title,
|
|
628
|
+
rows: group.rows.map((row) => ({
|
|
629
|
+
name: row.section,
|
|
630
|
+
href: row.href,
|
|
631
|
+
grade: row.grade,
|
|
632
|
+
before: row.before,
|
|
633
|
+
line: row.read,
|
|
634
|
+
})),
|
|
635
|
+
}))}
|
|
636
|
+
/>
|
|
616
637
|
);
|
|
617
638
|
}
|
|
618
639
|
|