@atelic-action/artifact 0.0.0-stage → 1.1.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 +153 -2
- package/package.json +68 -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 +544 -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 +2722 -0
- package/src/documents/index.ts +11 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { SignOff, type WriteupPerson } from "@atelic-action/ui/artifact";
|
|
2
|
+
import { Callout } from "@atelic-action/ui/components";
|
|
3
|
+
import {
|
|
4
|
+
type ArtifactLink,
|
|
5
|
+
type ArtifactParagraph,
|
|
6
|
+
isEmpty,
|
|
7
|
+
ArtifactAnchor as NoteAnchor,
|
|
8
|
+
Paragraph,
|
|
9
|
+
} from "./Paragraph";
|
|
10
|
+
import { Shot, type WriteupShot } from "./Shot";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The note: the first touch, for a business whose site publishes no email
|
|
14
|
+
* address, written as a short page and left at `<short>.atelic.me/notes`. It
|
|
15
|
+
* is the first touch email set as a document, so it carries no grades, no
|
|
16
|
+
* sections, and no tables: a title, the opener, what the customer sees and
|
|
17
|
+
* what sits under the hood, where the work would start, and the close. It
|
|
18
|
+
* reads on a phone first and prints to one letter sheet.
|
|
19
|
+
*
|
|
20
|
+
* It is the writeup's sibling, not a variant of it: a site wraps it in
|
|
21
|
+
* ArtifactShell exactly as it wraps the writeup, and it reuses the writeup's
|
|
22
|
+
* pieces where they fit (the Callout a finding is set in, the owner's screen,
|
|
23
|
+
* the sign off). Every prop is plain data, so a routine can write a note as
|
|
24
|
+
* JSON; the one exception is the sign off person's `role`, which takes a node
|
|
25
|
+
* and is equally happy with a string.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** A link inside running text: the words the reader clicks and where they go. */
|
|
29
|
+
export type NoteLink = ArtifactLink;
|
|
30
|
+
|
|
31
|
+
/** A paragraph: a plain string, or runs of text and links in order. */
|
|
32
|
+
export type NoteParagraph = ArtifactParagraph;
|
|
33
|
+
|
|
34
|
+
/** One defect and what it costs, with the owner's screen under it when there is one. */
|
|
35
|
+
export interface NoteFinding {
|
|
36
|
+
/** The defect, in a full sentence. */
|
|
37
|
+
lead: string;
|
|
38
|
+
/** The consequence: what it costs the business. */
|
|
39
|
+
body: string;
|
|
40
|
+
/**
|
|
41
|
+
* Where the defect can be seen for oneself. The lead links there: in a new
|
|
42
|
+
* tab for an http(s) link, in place otherwise.
|
|
43
|
+
*/
|
|
44
|
+
href?: string;
|
|
45
|
+
/** The owner's own screen, rendered under the finding. */
|
|
46
|
+
shot?: WriteupShot;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** One side of the read, e.g. "What your customer sees", with one or two findings. */
|
|
50
|
+
export interface NoteLane {
|
|
51
|
+
heading: string;
|
|
52
|
+
findings: NoteFinding[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** One place the work would start. */
|
|
56
|
+
export interface NoteStep {
|
|
57
|
+
/** The step, set in bold. */
|
|
58
|
+
lead: string;
|
|
59
|
+
/** What it does, after the lead. */
|
|
60
|
+
body?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Where the work would start: the line that introduces it, then the steps. */
|
|
64
|
+
export interface NoteStart {
|
|
65
|
+
/** The line over the steps, e.g. "I would start by:". */
|
|
66
|
+
lead: string;
|
|
67
|
+
items: NoteStep[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The close: who is writing and the offer, then the sign off. */
|
|
71
|
+
export interface NoteClose {
|
|
72
|
+
/** The sender paragraph, then the offer line with its scheduling link and the exit sentence. */
|
|
73
|
+
paragraphs: NoteParagraph[];
|
|
74
|
+
signOff: string;
|
|
75
|
+
person: WriteupPerson;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface NoteProps {
|
|
79
|
+
title: string;
|
|
80
|
+
/** The business, set in the dateline: "Prepared for <business>, <date>". */
|
|
81
|
+
preparedFor: string;
|
|
82
|
+
/** The ISO date, e.g. "2026-10-09". */
|
|
83
|
+
date: string;
|
|
84
|
+
/** The street sighting and a kudos, then the gap sentence. */
|
|
85
|
+
opener: NoteParagraph;
|
|
86
|
+
/** "What your customer sees" and "What I see under the hood", in that order. */
|
|
87
|
+
lanes: NoteLane[];
|
|
88
|
+
start: NoteStart;
|
|
89
|
+
close: NoteClose;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function Finding({ finding }: { finding: NoteFinding }) {
|
|
93
|
+
return (
|
|
94
|
+
<li className={finding.shot ? "nt-finding has-shot" : "nt-finding"}>
|
|
95
|
+
<Callout
|
|
96
|
+
tone="warn"
|
|
97
|
+
title={
|
|
98
|
+
finding.href ? <NoteAnchor href={finding.href}>{finding.lead}</NoteAnchor> : finding.lead
|
|
99
|
+
}
|
|
100
|
+
>
|
|
101
|
+
{finding.body}
|
|
102
|
+
</Callout>
|
|
103
|
+
{finding.shot && <Shot shot={finding.shot} />}
|
|
104
|
+
</li>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function Note({ title, preparedFor, date, opener, lanes, start, close }: NoteProps) {
|
|
109
|
+
const shownLanes = lanes.filter((lane) => lane.findings.length > 0);
|
|
110
|
+
const paragraphs = close.paragraphs.filter((paragraph) => !isEmpty(paragraph));
|
|
111
|
+
return (
|
|
112
|
+
<div className="nt">
|
|
113
|
+
<article className="nt-doc">
|
|
114
|
+
<header className="nt-head">
|
|
115
|
+
<p className="nt-meta">
|
|
116
|
+
Prepared for <b>{preparedFor}</b>, <span className="nt-meta-date">{date}</span>
|
|
117
|
+
</p>
|
|
118
|
+
<h1>{title}</h1>
|
|
119
|
+
</header>
|
|
120
|
+
{!isEmpty(opener) && <Paragraph paragraph={opener} className="nt-opener" />}
|
|
121
|
+
{shownLanes.length > 0 && (
|
|
122
|
+
<div className="nt-lanes">
|
|
123
|
+
{shownLanes.map((lane) => (
|
|
124
|
+
<section className="nt-lane" key={lane.heading}>
|
|
125
|
+
<h2>{lane.heading}</h2>
|
|
126
|
+
<ul className="nt-findings">
|
|
127
|
+
{lane.findings.map((finding) => (
|
|
128
|
+
<Finding finding={finding} key={finding.lead} />
|
|
129
|
+
))}
|
|
130
|
+
</ul>
|
|
131
|
+
</section>
|
|
132
|
+
))}
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
135
|
+
{start.items.length > 0 && (
|
|
136
|
+
<section className="nt-start">
|
|
137
|
+
<p className="nt-start-lead">{start.lead}</p>
|
|
138
|
+
<ul>
|
|
139
|
+
{start.items.map((step) => (
|
|
140
|
+
<li key={step.lead}>
|
|
141
|
+
<b>{step.lead}</b>
|
|
142
|
+
{step.body && <> {step.body}</>}
|
|
143
|
+
</li>
|
|
144
|
+
))}
|
|
145
|
+
</ul>
|
|
146
|
+
</section>
|
|
147
|
+
)}
|
|
148
|
+
<footer className="nt-close">
|
|
149
|
+
{paragraphs.map((paragraph, i) => (
|
|
150
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: paragraphs are static content in a fixed order
|
|
151
|
+
<Paragraph paragraph={paragraph} key={i} />
|
|
152
|
+
))}
|
|
153
|
+
<SignOff signOff={close.signOff} person={close.person} />
|
|
154
|
+
</footer>
|
|
155
|
+
</article>
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { newTabProps } from "@atelic-action/ui/artifact";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Plain data text for the documents a routine can write as JSON (the note
|
|
5
|
+
* and the read): a paragraph is a string, or runs of text and links in
|
|
6
|
+
* order. A link opens in a new tab when it leaves the page, and in place
|
|
7
|
+
* when it is an anchor or a mail link.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** A link inside running text: the words the reader clicks and where they go. */
|
|
11
|
+
export interface ArtifactLink {
|
|
12
|
+
text: string;
|
|
13
|
+
href: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** A paragraph: a plain string, or runs of text and links in order. */
|
|
17
|
+
export type ArtifactParagraph = string | Array<string | ArtifactLink>;
|
|
18
|
+
|
|
19
|
+
/** A link opens in a new tab when it leaves the page, and in place when it is an anchor or a mail link. */
|
|
20
|
+
export function ArtifactAnchor({ href, children }: { href: string; children: string }) {
|
|
21
|
+
return (
|
|
22
|
+
<a href={href} {...newTabProps(href, /^https?:/i.test(href))}>
|
|
23
|
+
{children}
|
|
24
|
+
</a>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A paragraph with nothing to read: a blank string, or runs that are all blank strings. A link always counts. */
|
|
29
|
+
export function isEmpty(paragraph: ArtifactParagraph): boolean {
|
|
30
|
+
const runs = typeof paragraph === "string" ? [paragraph] : paragraph;
|
|
31
|
+
return runs.every((run) => typeof run === "string" && run.trim() === "");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The runs alone, for a container that is not a paragraph (a list item, a table cell). */
|
|
35
|
+
export function Runs({ paragraph }: { paragraph: ArtifactParagraph }) {
|
|
36
|
+
const runs = typeof paragraph === "string" ? [paragraph] : paragraph;
|
|
37
|
+
return (
|
|
38
|
+
<>
|
|
39
|
+
{runs.map((run, i) =>
|
|
40
|
+
typeof run === "string" ? (
|
|
41
|
+
run
|
|
42
|
+
) : (
|
|
43
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: runs are static content in a fixed order
|
|
44
|
+
<ArtifactAnchor href={run.href} key={i}>
|
|
45
|
+
{run.text}
|
|
46
|
+
</ArtifactAnchor>
|
|
47
|
+
),
|
|
48
|
+
)}
|
|
49
|
+
</>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function Paragraph({
|
|
54
|
+
paragraph,
|
|
55
|
+
className,
|
|
56
|
+
}: {
|
|
57
|
+
paragraph: ArtifactParagraph;
|
|
58
|
+
className?: string;
|
|
59
|
+
}) {
|
|
60
|
+
return (
|
|
61
|
+
<p className={className}>
|
|
62
|
+
<Runs paragraph={paragraph} />
|
|
63
|
+
</p>
|
|
64
|
+
);
|
|
65
|
+
}
|