@atelic-action/artifact 0.0.0-stage → 1.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 +154 -2
- package/package.json +71 -5
- package/src/documents/Gate.tsx +342 -0
- package/src/documents/Note.tsx +158 -0
- package/src/documents/Paragraph.tsx +65 -0
- package/src/documents/Read.tsx +411 -0
- package/src/documents/Report.tsx +135 -0
- package/src/documents/Shot.tsx +38 -0
- package/src/documents/Writeup.tsx +771 -0
- package/src/documents/documents.css +2480 -0
- package/src/documents/index.ts +11 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import {
|
|
2
|
+
GradeChip,
|
|
3
|
+
SignOff,
|
|
4
|
+
type WriteupGrade,
|
|
5
|
+
type WriteupPerson,
|
|
6
|
+
} from "@atelic-action/ui/artifact";
|
|
7
|
+
import type { ReactNode } from "react";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The monthly page: what an Operate client gets each month, as one printed
|
|
11
|
+
* letter sheet and its hosted twin. Its shape is the practice's
|
|
12
|
+
* FOOTPRINT.md, under What the Owner Gets: the title and the period, then
|
|
13
|
+
* the leads block answering the four questions the Statement of Work
|
|
14
|
+
* promises, in that order, each a figure and one plain sentence; then the
|
|
15
|
+
* seven grades with what moved since last month and since the baseline; then
|
|
16
|
+
* the sign off. It wears the writeup's grade chip and sign off, and the
|
|
17
|
+
* artifact shell's header on screen; print drops the header and the credit
|
|
18
|
+
* band, so the sheet is the document alone.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export interface ReportLead {
|
|
22
|
+
/** The figure, e.g. "31 leads" or "$1,240". */
|
|
23
|
+
figure: ReactNode;
|
|
24
|
+
/** One plain sentence on what the figure means to the owner. */
|
|
25
|
+
sentence: ReactNode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The four questions, answered in this order on every monthly page. */
|
|
29
|
+
export interface ReportLeads {
|
|
30
|
+
cameIn: ReportLead;
|
|
31
|
+
fromWhere: ReportLead;
|
|
32
|
+
cost: ReportLead;
|
|
33
|
+
closed: ReportLead;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ReportGradeRow {
|
|
37
|
+
/** The tier, e.g. "Health". */
|
|
38
|
+
tier: string;
|
|
39
|
+
/** The section's trade name, e.g. "Local SEO". */
|
|
40
|
+
section: string;
|
|
41
|
+
/** The grade this month: the one judgement, in full color. */
|
|
42
|
+
now: WriteupGrade;
|
|
43
|
+
/** The grade last month; omit on the first month. */
|
|
44
|
+
lastMonth?: WriteupGrade;
|
|
45
|
+
/** The grade in the baseline snapshot, the first taken on private sources. */
|
|
46
|
+
baseline?: WriteupGrade;
|
|
47
|
+
/** One line on what moved, in the owner's words. */
|
|
48
|
+
moved: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ReportProps {
|
|
52
|
+
title: string;
|
|
53
|
+
/** The period as printed, e.g. "September 2026". */
|
|
54
|
+
period: string;
|
|
55
|
+
leads: ReportLeads;
|
|
56
|
+
/** The seven sections, in the footprint's order. */
|
|
57
|
+
grades: ReportGradeRow[];
|
|
58
|
+
/** The sign off, the same lockup the writeup closes on. */
|
|
59
|
+
close: { signOff: string; person: WriteupPerson };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The questions are the monthly page's contract with the Statement of Work,
|
|
64
|
+
* the same on every client's page, so they live here rather than in content.
|
|
65
|
+
*/
|
|
66
|
+
const QUESTIONS: { key: keyof ReportLeads; label: string }[] = [
|
|
67
|
+
{ key: "cameIn", label: "What Came In" },
|
|
68
|
+
{ key: "fromWhere", label: "From Where" },
|
|
69
|
+
{ key: "cost", label: "What It Cost" },
|
|
70
|
+
{ key: "closed", label: "What It Closed" },
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
function Earlier({ grade, label }: { grade?: WriteupGrade; label: string }) {
|
|
74
|
+
return (
|
|
75
|
+
<td className="rp-earlier" data-label={label}>
|
|
76
|
+
{grade && <GradeChip grade={grade} />}
|
|
77
|
+
</td>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function Report({ title, period, leads, grades, close }: ReportProps) {
|
|
82
|
+
let lastTier = "";
|
|
83
|
+
return (
|
|
84
|
+
<div className="rp">
|
|
85
|
+
<article className="rp-sheet">
|
|
86
|
+
<header className="rp-head">
|
|
87
|
+
<div className="rp-period">{period}</div>
|
|
88
|
+
<h1>{title}</h1>
|
|
89
|
+
</header>
|
|
90
|
+
<div className="rp-leads">
|
|
91
|
+
{QUESTIONS.map(({ key, label }) => (
|
|
92
|
+
<section className="rp-lead" key={key}>
|
|
93
|
+
<h2 className="rp-q">{label}</h2>
|
|
94
|
+
<div className="rp-figure">{leads[key].figure}</div>
|
|
95
|
+
<p>{leads[key].sentence}</p>
|
|
96
|
+
</section>
|
|
97
|
+
))}
|
|
98
|
+
</div>
|
|
99
|
+
<table className="rp-grades">
|
|
100
|
+
<thead>
|
|
101
|
+
<tr>
|
|
102
|
+
<th scope="col">Tier</th>
|
|
103
|
+
<th scope="col">Section</th>
|
|
104
|
+
<th scope="col">Now</th>
|
|
105
|
+
<th scope="col">Last Month</th>
|
|
106
|
+
<th scope="col">Baseline</th>
|
|
107
|
+
<th scope="col">What Moved</th>
|
|
108
|
+
</tr>
|
|
109
|
+
</thead>
|
|
110
|
+
<tbody>
|
|
111
|
+
{grades.map((row) => {
|
|
112
|
+
const tier = row.tier === lastTier ? "" : row.tier;
|
|
113
|
+
lastTier = row.tier;
|
|
114
|
+
return (
|
|
115
|
+
<tr key={row.section} className={tier ? "is-tier-start" : undefined}>
|
|
116
|
+
<td className="rp-tier">{tier}</td>
|
|
117
|
+
<td className="rp-section">{row.section}</td>
|
|
118
|
+
<td className="rp-now" data-label="Now">
|
|
119
|
+
<GradeChip grade={row.now} />
|
|
120
|
+
</td>
|
|
121
|
+
<Earlier grade={row.lastMonth} label="Last Month" />
|
|
122
|
+
<Earlier grade={row.baseline} label="Baseline" />
|
|
123
|
+
<td className="rp-moved">{row.moved}</td>
|
|
124
|
+
</tr>
|
|
125
|
+
);
|
|
126
|
+
})}
|
|
127
|
+
</tbody>
|
|
128
|
+
</table>
|
|
129
|
+
<footer className="rp-close">
|
|
130
|
+
<SignOff signOff={close.signOff} person={close.person} />
|
|
131
|
+
</footer>
|
|
132
|
+
</article>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { newTabProps } from "@atelic-action/ui/artifact";
|
|
2
|
+
import { Maximize2 } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
export interface WriteupShot {
|
|
5
|
+
/** The crop shown on the page. */
|
|
6
|
+
src: string;
|
|
7
|
+
alt: string;
|
|
8
|
+
/** The full screenshot the crop opens in a new tab; omit to render the crop alone. */
|
|
9
|
+
href?: string;
|
|
10
|
+
caption: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The owner's own screen: the crop, opening the full screenshot in a new tab
|
|
15
|
+
* when it has one, over its caption. The writeup and the note both wear it;
|
|
16
|
+
* its styles (.op-shot) are the writeup's, and the note only sizes it. A
|
|
17
|
+
* linked crop says it opens: a hover title, and an expand badge the note
|
|
18
|
+
* shows on its thumbnails and the writeup keeps hidden.
|
|
19
|
+
*/
|
|
20
|
+
export function Shot({ shot }: { shot: WriteupShot }) {
|
|
21
|
+
return (
|
|
22
|
+
<figure className="op-shot">
|
|
23
|
+
{shot.href ? (
|
|
24
|
+
<a href={shot.href} title="Open the full picture" {...newTabProps(shot.href, true)}>
|
|
25
|
+
<img src={shot.src} alt={shot.alt} loading="lazy" />
|
|
26
|
+
<span className="op-shot-open" aria-hidden="true">
|
|
27
|
+
<Maximize2 size={12} />
|
|
28
|
+
</span>
|
|
29
|
+
</a>
|
|
30
|
+
) : (
|
|
31
|
+
<span className="op-shot-frame">
|
|
32
|
+
<img src={shot.src} alt={shot.alt} loading="lazy" />
|
|
33
|
+
</span>
|
|
34
|
+
)}
|
|
35
|
+
<figcaption>{shot.caption}</figcaption>
|
|
36
|
+
</figure>
|
|
37
|
+
);
|
|
38
|
+
}
|