@atelic-action/artifact 1.2.0 → 2.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 +129 -201
- package/src/documents/Writeup.tsx +70 -19
- package/src/documents/documents.css +55 -170
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ The documents the practice hands or sends a business, as a published package and
|
|
|
7
7
|
|
|
8
8
|
Three layers, each a version the next depends on (ATE-671): `@atelic-action/ui` holds the pieces, this package holds the documents built from them, and each business's folder holds only its content, routes, and config. A document fix reaches a folder by bumping this package; a piece fix by bumping ui here, then this package in the folder.
|
|
9
9
|
|
|
10
|
-
Five artifacts live in the template site: the **writeup** at `/writeup`, the graded document an owner gets, a web page first with its printed copy derived from it; the **read** at `/read`, what a customer gets at the end of an engagement's first week, the baseline's grades explained by cause with the plan in order; the **proposal** at `/proposal`, sealed into the page and opened by the reader's own email address; the **monthly page** at `/report`, what an Operate client gets each month on one letter sheet; and the **note** at `/notes`, the first touch for a business whose site publishes no email address, set as a short page. The bare host sends a reader to the writeup. Every artifact prerenders to static HTML, stays out of every index, and carries the practice's brand by default.
|
|
10
|
+
Five artifacts live in the template site: the **writeup** at `/writeup`, the graded document an owner gets, a web page first with its printed copy derived from it; the **read** at `/the-read`, what a customer gets at the end of an engagement's first week, the baseline's grades explained by cause with the plan in order; the **proposal** at `/proposal`, sealed into the page and opened by the reader's own email address; the **monthly page** at `/report`, what an Operate client gets each month on one letter sheet; and the **note** at `/notes`, the first touch for a business whose site publishes no email address, set as a short page. The bare host sends a reader to the writeup. Every artifact prerenders to static HTML, stays out of every index, and carries the practice's brand by default.
|
|
11
11
|
|
|
12
12
|
The method behind them (the seven sections, the four grades from A to D, the rented line, what moved, and when an artifact is private) is the practice repo's `FOOTPRINT.md`. This repo builds to it and never restates it.
|
|
13
13
|
|
|
@@ -21,13 +21,14 @@ Every export of the package, all from the one entry, `@atelic-action/artifact`.
|
|
|
21
21
|
|
|
22
22
|
| Export | What it is | Route |
|
|
23
23
|
|---|---|---|
|
|
24
|
+
| `Topic` | The writeup's section card on its own: the trade name linked to its definition, the one liner, the grade, the why line, the blocks, and the sources, with an `extra` slot after the blocks. The read renders its sections through it | `/writeup` |
|
|
24
25
|
| `Writeup`, `WriteupProps` | The graded document an owner gets: the overview, the three tiers of section cards, the close | `/writeup` |
|
|
25
26
|
| `ratingCell` | A star rating table cell for a writeup table, sorted by its figure | `/writeup` |
|
|
26
27
|
| `WriteupBlock`, `WriteupTopic`, `WriteupPart`, `WriteupSheet`, `WriteupSummary`, `WriteupSummaryRow`, `WriteupMarkets`, `WriteupRented`, `WriteupMark`, `WriteupCell`, `WriteupTone`, and the rest of the `Writeup*` types | The shapes a writeup's content is written in | none |
|
|
27
28
|
| `WriteupGrade`, `WriteupPerson`, `WriteupShot` | The grade letter, the sign off person, and the owner's screen, shared by every document, all three re exported from ui | none |
|
|
28
|
-
| `Read`, `ReadProps` | The baseline's grades explained by cause: the gist table
|
|
29
|
-
| `readLabels`, `ReadLabels` | The fixed words the read sets (Prepared for, the gist table's columns,
|
|
30
|
-
| `ReadTier`, `ReadSection`, `
|
|
29
|
+
| `Read`, `ReadProps` | The baseline's grades explained by cause: the gist table, with an optional `note` callout under it for what the read leaves out; each section on the writeup's card (`ReadSection` extends `WriteupTopic`: the trade name linked to its definition, the one liner, the grade, the why line as the summary, the writeup's blocks, and the sources at the foot) plus the read's own today and after (a two column table) and what we recommend (a checklist); then the plan in order. A beat with no data is left out | `/the-read` |
|
|
30
|
+
| `readLabels`, `ReadLabels` | The fixed words the read sets (Prepared for, the gist table's columns, Today, After, Recommendations, the plan's heading), each overridable through `labels` | `/the-read` |
|
|
31
|
+
| `ReadTier`, `ReadSection`, `ReadChange`, `ReadStep`, `ReadPlan`, `ReadClose`, `ReadLink`, `ReadParagraph` | The shapes a read's content is written in. `ReadSection` is a `WriteupTopic` plus `gist`, `changes`, and `recommend`; its `why` and a finding's `body` take a paragraph (a string, or runs of text and links) as well as a node, so a routine can still write a read as JSON | none |
|
|
31
32
|
| `Report`, `ReportProps` | The monthly page: the four questions, the seven grades now and before, the sign off, on one letter sheet | `/report` |
|
|
32
33
|
| `ReportLead`, `ReportLeads`, `ReportGradeRow` | The shapes a monthly page's content is written in | none |
|
|
33
34
|
| `Note`, `NoteProps` | The first touch set as a page: the opener, two lanes of findings, where the work would start, the close | `/notes` |
|
|
@@ -44,7 +45,7 @@ What a document is built from stays in `@atelic-action/ui`, and this package imp
|
|
|
44
45
|
bun add @atelic-action/artifact @atelic-action/ui
|
|
45
46
|
```
|
|
46
47
|
|
|
47
|
-
Peer dependencies: `@atelic-action/ui` at `^2.
|
|
48
|
+
Peer dependencies: `@atelic-action/ui` at `^2.1.0`, `react` at `^19.0.0`, and `lucide-react` at `^1.23.0`. The package ships TSX and CSS as source, compiled inside the site's own Vite like ui's; a site whose `vite.config.ts` is one `artifactConfig` call from `@atelic-action/ui/vite` needs nothing more, for the build, the prerender, and the dev server alike. The default labels a document sets live in the document: the read's `readLabels` and the sealed document's gate copy are overridden through props, and the monthly page's four questions are fixed on purpose, since they are its contract with the Statement of Work.
|
|
48
49
|
|
|
49
50
|
```tsx
|
|
50
51
|
import { Writeup } from "@atelic-action/artifact";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atelic-action/artifact",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "The composed documents the Atelic practice hands or sends a business (the writeup, the read, the monthly page, the note, and the sealed document), built from the pieces in @atelic-action/ui",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,12 +37,12 @@
|
|
|
37
37
|
"format": "biome format . --write"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"@atelic-action/ui": "^2.
|
|
40
|
+
"@atelic-action/ui": "^2.1.0",
|
|
41
41
|
"lucide-react": "^1.23.0",
|
|
42
42
|
"react": "^19.0.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@atelic-action/ui": "^2.
|
|
45
|
+
"@atelic-action/ui": "^2.1.0",
|
|
46
46
|
"@biomejs/biome": "^2.0.0",
|
|
47
47
|
"@playwright/test": "^1.60.0",
|
|
48
48
|
"@sentry/react": "^10.0.0",
|
package/src/documents/Read.tsx
CHANGED
|
@@ -4,42 +4,43 @@ import {
|
|
|
4
4
|
GradeChip,
|
|
5
5
|
gradeLabel,
|
|
6
6
|
isEmpty,
|
|
7
|
-
newTabProps,
|
|
8
7
|
Paragraph,
|
|
9
8
|
Runs,
|
|
10
|
-
Shot,
|
|
11
9
|
SignOff,
|
|
12
10
|
type WriteupGrade,
|
|
13
11
|
type WriteupPerson,
|
|
14
|
-
type WriteupShot,
|
|
15
12
|
} from "@atelic-action/ui/artifact";
|
|
16
|
-
import {
|
|
13
|
+
import { IconList, type Tone } from "@atelic-action/ui/components";
|
|
14
|
+
import { Check } from "lucide-react";
|
|
15
|
+
import type { ReactNode } from "react";
|
|
16
|
+
import { Topic, type WriteupTopic } from "./Writeup";
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* The read: what a customer gets at the end of an engagement's first week,
|
|
20
20
|
* before anything changes. Where the writeup says what letter each section
|
|
21
21
|
* earned, the read says why, and what changes. Its shape is the practice's
|
|
22
22
|
* FOOTPRINT.md, under What the Owner Gets: a title and a dated lede; the gist
|
|
23
|
-
* table (every section, its grade, and one line)
|
|
24
|
-
*
|
|
23
|
+
* table (every section, its grade, and one line), with a note under it for
|
|
24
|
+
* what the read leaves out; then the tiers; then the plan, in order; then one
|
|
25
|
+
* closing line and the sign off.
|
|
25
26
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* of
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
27
|
+
* A section is the writeup's section card, the same `Topic`, so the two
|
|
28
|
+
* documents read alike (Forni, 2026-10-16): the trade name linked to its
|
|
29
|
+
* definition, the one liner, the grade, and the why line, which here is the
|
|
30
|
+
* summary of what we saw in one or two sentences; then the writeup's blocks
|
|
31
|
+
* (findings with the owner's screens beside them, the basics, a table, a
|
|
32
|
+
* fold); then what is the read's own, today and after as a two column table
|
|
33
|
+
* and what we recommend as a checklist; and the tools we looked through at
|
|
34
|
+
* the foot, as the sources. A beat with nothing in it is left out.
|
|
33
35
|
*
|
|
34
|
-
* It
|
|
35
|
-
*
|
|
36
|
-
* (the `.
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* is plain data,
|
|
41
|
-
*
|
|
42
|
-
* a string.
|
|
36
|
+
* It wears the writeup's column, head, grade chip and key, tier heading,
|
|
37
|
+
* section card, and close (the `.op` classes), and adds only what is its own
|
|
38
|
+
* (the `.rd` classes). The gist table is drawn from the tiers, so a tier
|
|
39
|
+
* added later (Conversion, once the analytics access lands) is one more entry
|
|
40
|
+
* in `tiers` and appears in the table and the body alike; until then `note`
|
|
41
|
+
* says so under the table. Every prop but a finding's body and the sign off
|
|
42
|
+
* person's `role` is plain data, and both are equally happy with a string, so
|
|
43
|
+
* a routine can write a read as JSON.
|
|
43
44
|
*/
|
|
44
45
|
|
|
45
46
|
/** A link inside running text: the words the reader clicks and where they go. */
|
|
@@ -48,65 +49,36 @@ export type ReadLink = ArtifactLink;
|
|
|
48
49
|
/** A paragraph: a plain string, or runs of text and links in order. */
|
|
49
50
|
export type ReadParagraph = ArtifactParagraph;
|
|
50
51
|
|
|
51
|
-
/** A tool we looked through: its name, where it opens (in a new tab), and a line on what we read there. */
|
|
52
|
-
export interface ReadTool {
|
|
53
|
-
label: string;
|
|
54
|
-
href?: string;
|
|
55
|
-
/** One short line, e.g. "Page indexing, the last 90 days". */
|
|
56
|
-
note?: string;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** The 1.0.0 name for a tool; the same shape. */
|
|
60
|
-
export type ReadSource = ReadTool;
|
|
61
|
-
|
|
62
|
-
/** One row of a section's facts: a short label and its value, e.g. "Pages on Google" and "9 of 9". */
|
|
63
|
-
export interface ReadFact {
|
|
64
|
-
label: string;
|
|
65
|
-
value: ReadParagraph;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** A list of examples under its own short heading; each item a paragraph that can carry links. */
|
|
69
|
-
export interface ReadExamples {
|
|
70
|
-
title?: string;
|
|
71
|
-
items: ReadParagraph[];
|
|
72
|
-
}
|
|
73
|
-
|
|
74
52
|
/** One row of today and after: what a thing is now, and what it becomes. */
|
|
75
53
|
export interface ReadChange {
|
|
76
54
|
now: ReadParagraph;
|
|
77
55
|
after: ReadParagraph;
|
|
78
56
|
}
|
|
79
57
|
|
|
80
|
-
/**
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
/** The question the section answers, in the owner's words, e.g. "Can Google read the site?". The section's heading. */
|
|
87
|
-
question: string;
|
|
88
|
-
/** The baseline's grade. Omit for a section not yet graded; the table shows a quiet dash. */
|
|
89
|
-
grade?: WriteupGrade;
|
|
58
|
+
/**
|
|
59
|
+
* One section of a tier: the writeup's section card (its `title`, `href`,
|
|
60
|
+
* `oneLiner`, `grade`, `why`, `blocks`, and `sources`), plus the read's own:
|
|
61
|
+
* the gist table's line, today and after, and what we recommend.
|
|
62
|
+
*/
|
|
63
|
+
export interface ReadSection extends Omit<WriteupTopic, "kind" | "why"> {
|
|
90
64
|
/** The one line in the gist table. */
|
|
91
65
|
gist: ReadParagraph;
|
|
92
|
-
/**
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
sources?: ReadTool[];
|
|
96
|
-
/** What it showed: one or two short paragraphs. */
|
|
97
|
-
found: ReadParagraph[];
|
|
98
|
-
/** What it showed, as a compact two column table of facts under the paragraphs. */
|
|
99
|
-
facts?: ReadFact[];
|
|
100
|
-
/** What it showed, as lists of examples, each under its own short heading. */
|
|
101
|
-
examples?: ReadExamples[];
|
|
102
|
-
/** Proof pictures, each over its caption, e.g. a search result capture per market. */
|
|
103
|
-
shots?: WriteupShot[];
|
|
104
|
-
/** Why, in one sentence, set apart. */
|
|
105
|
-
why: ReadParagraph;
|
|
106
|
-
/** Today and after: what each thing is, and what it becomes. */
|
|
66
|
+
/** The summary of what we saw, in one or two sentences: the why line. A paragraph, so a read written as JSON can carry links in it. */
|
|
67
|
+
why?: ReactNode | ReadParagraph;
|
|
68
|
+
/** Today and after: what each thing is, and what it becomes. Set after the blocks. */
|
|
107
69
|
changes?: ReadChange[];
|
|
108
|
-
/**
|
|
109
|
-
recommend?: ReadParagraph[];
|
|
70
|
+
/** The recommendations, after today and after: a paragraph each, or one with its own icon and tone. */
|
|
71
|
+
recommend?: (ReadParagraph | ReadRecommendation)[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** One recommendation with a bullet of its own: the icon it leads with, and the tone it reads in. Both fall back to the list's. */
|
|
75
|
+
export interface ReadRecommendation {
|
|
76
|
+
/** The recommendation: a paragraph, or a node when it carries a table or a list. */
|
|
77
|
+
body: ReactNode | ReadParagraph;
|
|
78
|
+
/** The icon as the bullet, e.g. a lucide icon at size 16; the accent's check when left out. */
|
|
79
|
+
icon?: ReactNode;
|
|
80
|
+
/** How the item reads; the accent when left out. */
|
|
81
|
+
tone?: Tone;
|
|
110
82
|
}
|
|
111
83
|
|
|
112
84
|
/** A tier, e.g. Health: its heading, the question it answers, and its sections in order. */
|
|
@@ -115,7 +87,7 @@ export interface ReadTier {
|
|
|
115
87
|
id?: string;
|
|
116
88
|
/** The tier's name, e.g. "Health". */
|
|
117
89
|
title: string;
|
|
118
|
-
/** The
|
|
90
|
+
/** The questions the tier answers, in the owner's words. */
|
|
119
91
|
lead?: string;
|
|
120
92
|
sections: ReadSection[];
|
|
121
93
|
}
|
|
@@ -159,18 +131,16 @@ export interface ReadLabels {
|
|
|
159
131
|
gist: string;
|
|
160
132
|
/** What a screen reader hears for a section with no grade. */
|
|
161
133
|
notGraded: string;
|
|
162
|
-
/** The beat at the top of a section: the tools we looked through. */
|
|
163
|
-
tools: string;
|
|
164
|
-
/** The beat that says what the tools showed. */
|
|
165
|
-
found: string;
|
|
166
|
-
/** The one sentence beat. */
|
|
167
|
-
why: string;
|
|
168
134
|
/** The today and after table's first column. */
|
|
169
135
|
now: string;
|
|
170
136
|
/** The today and after table's second column. */
|
|
171
137
|
after: string;
|
|
172
138
|
/** The checklist beat at the foot of a section. */
|
|
173
139
|
recommend: string;
|
|
140
|
+
/** The word before the grade on a section's why line. */
|
|
141
|
+
why: string;
|
|
142
|
+
/** The label over a section's tools when it names none. */
|
|
143
|
+
sources: string;
|
|
174
144
|
/** The plan's heading when it names none. */
|
|
175
145
|
plan: string;
|
|
176
146
|
}
|
|
@@ -181,12 +151,11 @@ export const readLabels: ReadLabels = {
|
|
|
181
151
|
grade: "Grade",
|
|
182
152
|
gist: "The Gist",
|
|
183
153
|
notGraded: "Not graded yet",
|
|
184
|
-
tools: "Where We Looked",
|
|
185
|
-
found: "What It Showed",
|
|
186
|
-
why: "Why",
|
|
187
154
|
now: "Today",
|
|
188
155
|
after: "After",
|
|
189
|
-
recommend: "
|
|
156
|
+
recommend: "Recommendations",
|
|
157
|
+
why: "Why",
|
|
158
|
+
sources: "See it yourself",
|
|
190
159
|
plan: "The Plan",
|
|
191
160
|
};
|
|
192
161
|
|
|
@@ -200,6 +169,8 @@ export interface ReadProps {
|
|
|
200
169
|
lede: ReadParagraph;
|
|
201
170
|
/** The heading over the gist table, e.g. "At a Glance". */
|
|
202
171
|
gistTitle?: string;
|
|
172
|
+
/** A callout under the gist table, e.g. what the read leaves out and when it follows. */
|
|
173
|
+
note?: ReadParagraph;
|
|
203
174
|
/** The key: what each letter means, in a few words each. */
|
|
204
175
|
gradeKey?: Array<{ grade: WriteupGrade; label: string }>;
|
|
205
176
|
/** The tiers in order. Each section appears in the gist table and in the body. */
|
|
@@ -214,51 +185,29 @@ function filled(paragraph: ReadParagraph | undefined | null): paragraph is ReadP
|
|
|
214
185
|
return paragraph !== undefined && paragraph !== null && !isEmpty(paragraph);
|
|
215
186
|
}
|
|
216
187
|
|
|
217
|
-
|
|
188
|
+
/** A string or runs of text and links, the shape JSON content arrives in; a node of elements is anything else. */
|
|
189
|
+
function isRuns(value: unknown): value is ReadParagraph {
|
|
190
|
+
if (typeof value === "string") return true;
|
|
218
191
|
return (
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
{tool.href ? (
|
|
226
|
-
<a href={tool.href} {...newTabProps(tool.href, true)}>
|
|
227
|
-
{tool.label}
|
|
228
|
-
<ArrowUpRight size={13} aria-hidden="true" />
|
|
229
|
-
</a>
|
|
230
|
-
) : (
|
|
231
|
-
<span className="rd-tool-name">{tool.label}</span>
|
|
232
|
-
)}
|
|
233
|
-
{tool.note && <span className="rd-tool-note">{tool.note}</span>}
|
|
234
|
-
</li>
|
|
235
|
-
))}
|
|
236
|
-
</ul>
|
|
237
|
-
</div>
|
|
192
|
+
Array.isArray(value) &&
|
|
193
|
+
value.every(
|
|
194
|
+
(run) =>
|
|
195
|
+
typeof run === "string" ||
|
|
196
|
+
(typeof run === "object" && run !== null && "text" in run && "href" in run),
|
|
197
|
+
)
|
|
238
198
|
);
|
|
239
199
|
}
|
|
240
200
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
// biome-ignore lint/suspicious/noArrayIndexKey: facts are static content in a fixed order
|
|
247
|
-
<tr key={i}>
|
|
248
|
-
<th scope="row">{fact.label}</th>
|
|
249
|
-
<td>
|
|
250
|
-
<Runs paragraph={fact.value} />
|
|
251
|
-
</td>
|
|
252
|
-
</tr>
|
|
253
|
-
))}
|
|
254
|
-
</tbody>
|
|
255
|
-
</table>
|
|
256
|
-
);
|
|
201
|
+
/** Content as a node: runs are rendered, a blank is dropped, and a node passes through. */
|
|
202
|
+
function asNode(value: ReactNode | ReadParagraph | undefined | null): ReactNode {
|
|
203
|
+
if (value === undefined || value === null) return undefined;
|
|
204
|
+
if (isRuns(value)) return filled(value) ? <Runs paragraph={value} /> : undefined;
|
|
205
|
+
return value;
|
|
257
206
|
}
|
|
258
207
|
|
|
259
208
|
function Changes({ changes, labels }: { changes: ReadChange[]; labels: ReadLabels }) {
|
|
260
209
|
return (
|
|
261
|
-
<div className="rd-beat rd-changes">
|
|
210
|
+
<div className="op-block rd-beat rd-changes">
|
|
262
211
|
<table className="rd-changes-table">
|
|
263
212
|
<thead>
|
|
264
213
|
<tr>
|
|
@@ -288,21 +237,24 @@ function Changes({ changes, labels }: { changes: ReadChange[]; labels: ReadLabel
|
|
|
288
237
|
);
|
|
289
238
|
}
|
|
290
239
|
|
|
291
|
-
|
|
240
|
+
/** A recommendation as its full shape, whichever way the content wrote it. */
|
|
241
|
+
function asRecommendation(item: ReadParagraph | ReadRecommendation): ReadRecommendation {
|
|
242
|
+
return isRuns(item) ? { body: item } : item;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function Recommend({ items, label }: { items: ReadRecommendation[]; label: string }) {
|
|
292
246
|
return (
|
|
293
|
-
<div className="rd-beat rd-recommend">
|
|
247
|
+
<div className="op-block rd-beat rd-recommend">
|
|
294
248
|
<h4>{label}</h4>
|
|
295
|
-
<
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
))}
|
|
305
|
-
</ul>
|
|
249
|
+
<IconList
|
|
250
|
+
className="rd-checks"
|
|
251
|
+
icon={<Check size={16} strokeWidth={2.5} />}
|
|
252
|
+
items={items.map((item) => ({
|
|
253
|
+
icon: item.icon,
|
|
254
|
+
tone: item.tone,
|
|
255
|
+
body: asNode(item.body),
|
|
256
|
+
}))}
|
|
257
|
+
/>
|
|
306
258
|
</div>
|
|
307
259
|
);
|
|
308
260
|
}
|
|
@@ -361,7 +313,7 @@ function Gist({
|
|
|
361
313
|
{tier.sections.map((section) => (
|
|
362
314
|
<tr key={section.id}>
|
|
363
315
|
<th scope="row" className="rd-gist-section">
|
|
364
|
-
<a href={`#${section.id}`}>{section.
|
|
316
|
+
<a href={`#${section.id}`}>{section.title}</a>
|
|
365
317
|
</th>
|
|
366
318
|
<td className="rd-gist-grade">
|
|
367
319
|
{section.grade ? (
|
|
@@ -391,77 +343,45 @@ function Gist({
|
|
|
391
343
|
);
|
|
392
344
|
}
|
|
393
345
|
|
|
346
|
+
/** A section: the writeup's card, with today and after and the checklist handed in after its blocks. */
|
|
394
347
|
function Section({ section, labels }: { section: ReadSection; labels: ReadLabels }) {
|
|
395
|
-
const tools = (section.tools ?? section.sources ?? []).filter((tool) => tool?.label?.trim());
|
|
396
|
-
const found = (section.found ?? []).filter(filled);
|
|
397
|
-
const facts = (section.facts ?? []).filter((fact) => filled(fact?.value));
|
|
398
|
-
const examples = (section.examples ?? [])
|
|
399
|
-
.map((list) => ({ title: list?.title, items: (list?.items ?? []).filter(filled) }))
|
|
400
|
-
.filter((list) => list.items.length > 0);
|
|
401
|
-
const shots = section.shots ?? [];
|
|
402
348
|
const changes = (section.changes ?? []).filter(
|
|
403
349
|
(change) => filled(change?.now) || filled(change?.after),
|
|
404
350
|
);
|
|
405
|
-
const recommend = (section.recommend ?? [])
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
<GradeChip grade={section.grade} />
|
|
419
|
-
</span>
|
|
420
|
-
</>
|
|
421
|
-
)}
|
|
422
|
-
</header>
|
|
423
|
-
{tools.length > 0 && <Tools tools={tools} label={labels.tools} />}
|
|
424
|
-
{showed && (
|
|
425
|
-
<div className="rd-beat rd-found">
|
|
426
|
-
<h4>{labels.found}</h4>
|
|
427
|
-
{found.map((paragraph, i) => (
|
|
428
|
-
// biome-ignore lint/suspicious/noArrayIndexKey: paragraphs are static content in a fixed order
|
|
429
|
-
<Paragraph paragraph={paragraph} key={i} />
|
|
430
|
-
))}
|
|
431
|
-
{facts.length > 0 && <Facts facts={facts} />}
|
|
432
|
-
{examples.map((list, i) => (
|
|
433
|
-
// biome-ignore lint/suspicious/noArrayIndexKey: lists are static content in a fixed order, and a title may repeat
|
|
434
|
-
<div className="rd-examples-block" key={i}>
|
|
435
|
-
{list.title && <h5>{list.title}</h5>}
|
|
436
|
-
<ul className="rd-examples">
|
|
437
|
-
{list.items.map((example, j) => (
|
|
438
|
-
// biome-ignore lint/suspicious/noArrayIndexKey: examples are static content in a fixed order
|
|
439
|
-
<li key={j}>
|
|
440
|
-
<Runs paragraph={example} />
|
|
441
|
-
</li>
|
|
442
|
-
))}
|
|
443
|
-
</ul>
|
|
444
|
-
</div>
|
|
445
|
-
))}
|
|
446
|
-
{shots.length > 0 && (
|
|
447
|
-
<div className="rd-shots" data-count={Math.min(shots.length, 3)}>
|
|
448
|
-
{shots.map((shot) => (
|
|
449
|
-
<Shot shot={shot} key={shot.src} />
|
|
450
|
-
))}
|
|
451
|
-
</div>
|
|
452
|
-
)}
|
|
453
|
-
</div>
|
|
454
|
-
)}
|
|
455
|
-
{filled(section.why) && (
|
|
456
|
-
<div className="rd-beat rd-why">
|
|
457
|
-
<h4>{labels.why}</h4>
|
|
458
|
-
<Paragraph paragraph={section.why} />
|
|
459
|
-
</div>
|
|
460
|
-
)}
|
|
461
|
-
{changes.length > 0 && <Changes changes={changes} labels={labels} />}
|
|
462
|
-
{recommend.length > 0 && <Recommend items={recommend} label={labels.recommend} />}
|
|
463
|
-
</section>
|
|
351
|
+
const recommend = (section.recommend ?? [])
|
|
352
|
+
.filter((item) => item !== undefined && item !== null)
|
|
353
|
+
.map(asRecommendation)
|
|
354
|
+
.filter((item) => (isRuns(item.body) ? filled(item.body) : item.body != null));
|
|
355
|
+
const blocks = (section.blocks ?? []).map((block) =>
|
|
356
|
+
block.kind === "findings"
|
|
357
|
+
? {
|
|
358
|
+
...block,
|
|
359
|
+
items: block.items
|
|
360
|
+
.filter((item) => item?.lead?.trim())
|
|
361
|
+
.map((item) => ({ ...item, body: asNode(item.body) })),
|
|
362
|
+
}
|
|
363
|
+
: block,
|
|
464
364
|
);
|
|
365
|
+
const sources = (section.sources ?? []).filter((source) => source?.label?.trim());
|
|
366
|
+
const topic: WriteupTopic = {
|
|
367
|
+
kind: "topic",
|
|
368
|
+
...section,
|
|
369
|
+
why: asNode(section.why),
|
|
370
|
+
whyLabel: section.whyLabel ?? labels.why,
|
|
371
|
+
gradeWord: section.gradeWord ?? labels.grade,
|
|
372
|
+
blocks,
|
|
373
|
+
sources,
|
|
374
|
+
sourcesLabel: section.sourcesLabel ?? labels.sources,
|
|
375
|
+
noShot: section.noShot ?? true,
|
|
376
|
+
};
|
|
377
|
+
const extra =
|
|
378
|
+
changes.length + recommend.length > 0 ? (
|
|
379
|
+
<>
|
|
380
|
+
{changes.length > 0 && <Changes changes={changes} labels={labels} />}
|
|
381
|
+
{recommend.length > 0 && <Recommend items={recommend} label={labels.recommend} />}
|
|
382
|
+
</>
|
|
383
|
+
) : undefined;
|
|
384
|
+
return <Topic topic={topic} extra={extra} />;
|
|
465
385
|
}
|
|
466
386
|
|
|
467
387
|
function Plan({ plan, labels }: { plan: ReadPlan; labels: ReadLabels }) {
|
|
@@ -500,6 +420,7 @@ export function Read({
|
|
|
500
420
|
date,
|
|
501
421
|
lede,
|
|
502
422
|
gistTitle,
|
|
423
|
+
note,
|
|
503
424
|
gradeKey,
|
|
504
425
|
tiers,
|
|
505
426
|
plan,
|
|
@@ -523,7 +444,14 @@ export function Read({
|
|
|
523
444
|
{filled(lede) && <Paragraph paragraph={lede} className="op-intro rd-lede" />}
|
|
524
445
|
</header>
|
|
525
446
|
{shownTiers.length > 0 && (
|
|
526
|
-
|
|
447
|
+
<>
|
|
448
|
+
<Gist tiers={shownTiers} title={gistTitle} gradeKey={gradeKey} labels={labels} />
|
|
449
|
+
{filled(note) && (
|
|
450
|
+
<div className="rd-note" role="note">
|
|
451
|
+
<Paragraph paragraph={note} />
|
|
452
|
+
</div>
|
|
453
|
+
)}
|
|
454
|
+
</>
|
|
527
455
|
)}
|
|
528
456
|
{shownTiers.map((tier) => (
|
|
529
457
|
<div className="rd-tier" key={tier.title}>
|
|
@@ -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,
|
|
@@ -52,8 +54,8 @@ export type WriteupTone = Tone;
|
|
|
52
54
|
export interface WriteupBullet {
|
|
53
55
|
/** The point, in a full sentence. The icon sits beside it. */
|
|
54
56
|
lead: string;
|
|
55
|
-
/** The one explainer that says why it matters. */
|
|
56
|
-
body?: ReactNode;
|
|
57
|
+
/** The one explainer that says why it matters: a node, or a paragraph of runs when the content arrives as JSON. */
|
|
58
|
+
body?: ReactNode | ArtifactParagraph;
|
|
57
59
|
/** A win the owner already has: marked good. Everything else reads as needing work. */
|
|
58
60
|
win?: boolean;
|
|
59
61
|
/** Overrides the tone, e.g. "info" for something not yet read. */
|
|
@@ -113,6 +115,8 @@ export interface WriteupTable extends BlockBase {
|
|
|
113
115
|
sortable?: boolean;
|
|
114
116
|
/** The direction a first click sorts in. */
|
|
115
117
|
firstSort?: "asc" | "desc";
|
|
118
|
+
/** The column hugs its content, for a swatch or a mark. */
|
|
119
|
+
fit?: boolean;
|
|
116
120
|
}>;
|
|
117
121
|
rows: Array<{ cells: WriteupCell[]; tone?: WriteupTone }>;
|
|
118
122
|
/** The sort the table opens in. */
|
|
@@ -199,8 +203,12 @@ export interface WriteupTopic {
|
|
|
199
203
|
oneLiner: string;
|
|
200
204
|
/** The section's grade, beside the heading. */
|
|
201
205
|
grade?: WriteupGrade;
|
|
202
|
-
/** Why it earned that letter: the rule it met, in a sentence. */
|
|
206
|
+
/** Why it earned that letter: the rule it met, in a sentence. The read sets its summary here. */
|
|
203
207
|
why?: ReactNode;
|
|
208
|
+
/** The word before the grade on the why line; defaults to "Why". */
|
|
209
|
+
whyLabel?: string;
|
|
210
|
+
/** What a screen reader hears before the grade's letter; defaults to "Grade". */
|
|
211
|
+
gradeWord?: string;
|
|
204
212
|
/** The section's blocks, in order. */
|
|
205
213
|
blocks?: WriteupTopicBlock[];
|
|
206
214
|
/** Findings without blocks: rendered as one findings block with the shots beside it. */
|
|
@@ -388,21 +396,43 @@ function Table({ block }: { block: WriteupTable }) {
|
|
|
388
396
|
);
|
|
389
397
|
}
|
|
390
398
|
|
|
399
|
+
/** A string or runs of text and links, the shape JSON content arrives in; a node of elements is anything else. */
|
|
400
|
+
function isRuns(value: unknown): value is ArtifactParagraph {
|
|
401
|
+
if (typeof value === "string") return true;
|
|
402
|
+
return (
|
|
403
|
+
Array.isArray(value) &&
|
|
404
|
+
value.every(
|
|
405
|
+
(run) =>
|
|
406
|
+
typeof run === "string" ||
|
|
407
|
+
(typeof run === "object" && run !== null && "text" in run && "href" in run),
|
|
408
|
+
)
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** A finding's body as a node, whichever way the content wrote it. */
|
|
413
|
+
function bodyNode(body: ReactNode | ArtifactParagraph | undefined): ReactNode {
|
|
414
|
+
if (body === undefined || body === null) return undefined;
|
|
415
|
+
return isRuns(body) ? <Runs paragraph={body} /> : body;
|
|
416
|
+
}
|
|
417
|
+
|
|
391
418
|
function Findings({ block, brief }: { block: WriteupFindings; brief?: string }) {
|
|
392
419
|
const hasShots = Boolean(block.shot || brief);
|
|
393
420
|
return (
|
|
394
421
|
<div className={`op-block op-findings${hasShots ? " has-shots" : ""}`}>
|
|
395
422
|
<div className="op-findings-list">
|
|
396
|
-
{block.items.map((item) =>
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
423
|
+
{block.items.map((item, i) => {
|
|
424
|
+
const shape = {
|
|
425
|
+
tone: item.tone ?? (item.win ? "good" : "warn"),
|
|
426
|
+
title: item.lead,
|
|
427
|
+
variant: item.box ? ("box" as const) : ("plain" as const),
|
|
428
|
+
};
|
|
429
|
+
return (
|
|
430
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: findings are static content in a fixed order, and a lead may repeat
|
|
431
|
+
<Callout key={i} {...shape}>
|
|
432
|
+
{bodyNode(item.body)}
|
|
433
|
+
</Callout>
|
|
434
|
+
);
|
|
435
|
+
})}
|
|
406
436
|
</div>
|
|
407
437
|
{hasShots && (
|
|
408
438
|
<div className="op-shots">
|
|
@@ -495,7 +525,14 @@ function TopicBlock({ block }: { block: WriteupTopicBlock }) {
|
|
|
495
525
|
}
|
|
496
526
|
}
|
|
497
527
|
|
|
498
|
-
|
|
528
|
+
/**
|
|
529
|
+
* The section card: the trade name linked to its definition, the one liner,
|
|
530
|
+
* the why line, the grade, the blocks, and the sources at the foot. The read
|
|
531
|
+
* renders its sections through it too, handing anything of its own (today and
|
|
532
|
+
* after, what we recommend) as `extra`, set after the blocks and before the
|
|
533
|
+
* sources, so the two documents share one layout.
|
|
534
|
+
*/
|
|
535
|
+
export function Topic({ topic, extra }: { topic: WriteupTopic; extra?: ReactNode }) {
|
|
499
536
|
const legacy: WriteupFindings | null = topic.bullets
|
|
500
537
|
? { kind: "findings", items: topic.bullets, shot: topic.shot, shot2: topic.shot2 }
|
|
501
538
|
: null;
|
|
@@ -519,13 +556,25 @@ function Topic({ topic }: { topic: WriteupTopic }) {
|
|
|
519
556
|
)}
|
|
520
557
|
</h3>
|
|
521
558
|
<p className="op-def">{topic.oneLiner}</p>
|
|
522
|
-
{topic.
|
|
559
|
+
{topic.why && (
|
|
523
560
|
<p className="op-why">
|
|
524
|
-
<b>
|
|
561
|
+
<b>
|
|
562
|
+
{topic.grade
|
|
563
|
+
? `${topic.whyLabel ?? "Why"} ${gradeLabel[topic.grade]}:`
|
|
564
|
+
: `${topic.whyLabel ?? "Why"}:`}
|
|
565
|
+
</b>{" "}
|
|
566
|
+
{topic.why}
|
|
525
567
|
</p>
|
|
526
568
|
)}
|
|
527
569
|
</div>
|
|
528
|
-
{topic.grade &&
|
|
570
|
+
{topic.grade && (
|
|
571
|
+
<>
|
|
572
|
+
<span className="op-sr-only">{`${topic.gradeWord ?? "Grade"} ${gradeLabel[topic.grade]}`}</span>
|
|
573
|
+
<span aria-hidden="true">
|
|
574
|
+
<GradeChip grade={topic.grade} />
|
|
575
|
+
</span>
|
|
576
|
+
</>
|
|
577
|
+
)}
|
|
529
578
|
</header>
|
|
530
579
|
<div className="op-blocks">
|
|
531
580
|
{topic.blocks?.map((block, i) => (
|
|
@@ -533,13 +582,15 @@ function Topic({ topic }: { topic: WriteupTopic }) {
|
|
|
533
582
|
<TopicBlock block={block} key={i} />
|
|
534
583
|
))}
|
|
535
584
|
{legacy && <Findings block={legacy} brief={brief} />}
|
|
585
|
+
{extra}
|
|
536
586
|
</div>
|
|
537
587
|
{topic.sources && topic.sources.length > 0 && (
|
|
538
588
|
<footer className="op-sources">
|
|
539
589
|
<span className="op-sources-label">{topic.sourcesLabel ?? "Sources"}</span>
|
|
540
590
|
<ul>
|
|
541
|
-
{topic.sources.map((source) => (
|
|
542
|
-
|
|
591
|
+
{topic.sources.map((source, i) => (
|
|
592
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: sources are static content in a fixed order, and a label may repeat
|
|
593
|
+
<li key={i}>
|
|
543
594
|
{source.href ? (
|
|
544
595
|
<a href={source.href} {...newTabProps(source.href, true)}>
|
|
545
596
|
{source.label}
|
|
@@ -1126,7 +1126,7 @@
|
|
|
1126
1126
|
font-weight: 650;
|
|
1127
1127
|
color: var(--op-ink);
|
|
1128
1128
|
}
|
|
1129
|
-
.atelic-ui .op-topic-head
|
|
1129
|
+
.atelic-ui .op-topic-head .op-grade {
|
|
1130
1130
|
width: 42px;
|
|
1131
1131
|
height: 42px;
|
|
1132
1132
|
border-radius: 12px;
|
|
@@ -1582,7 +1582,7 @@
|
|
|
1582
1582
|
.atelic-ui .op-topic h3 {
|
|
1583
1583
|
font-size: 23px;
|
|
1584
1584
|
}
|
|
1585
|
-
.atelic-ui .op-topic-head
|
|
1585
|
+
.atelic-ui .op-topic-head .op-grade {
|
|
1586
1586
|
width: 46px;
|
|
1587
1587
|
height: 46px;
|
|
1588
1588
|
border-radius: 13px;
|
|
@@ -2160,17 +2160,22 @@
|
|
|
2160
2160
|
column, the head, the grade chip and its key, the tier heading, the
|
|
2161
2161
|
section card, the screen, the close, and every print rule of the
|
|
2162
2162
|
writeup's are its own already. This block adds only what the read has
|
|
2163
|
-
and the writeup does not: the gist table
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
pictures in a row, and the numbered plan. It sets no @page; the writeup's, earlier in this sheet and the
|
|
2163
|
+
and the writeup does not: the gist table and the note under it, the
|
|
2164
|
+
two beats a section adds to the writeup's card (today and after, and
|
|
2165
|
+
what we recommend), and the numbered plan. It sets no @page; the writeup's, earlier in this sheet and the
|
|
2167
2166
|
later of the two there, sets letter paper for every artifact page.
|
|
2168
2167
|
|
|
2169
2168
|
Tokens only (theme.css).
|
|
2170
2169
|
================================================================= */
|
|
2171
2170
|
|
|
2171
|
+
/* The lede runs the full width of the page; the writeup's intro is the one that is capped. */
|
|
2172
|
+
.atelic-ui .rd-lede {
|
|
2173
|
+
max-width: none;
|
|
2174
|
+
}
|
|
2172
2175
|
/* Prose links read as links here, where the writeup's are quiet. */
|
|
2173
2176
|
.atelic-ui .rd-lede a,
|
|
2177
|
+
.atelic-ui .rd-note a,
|
|
2178
|
+
.atelic-ui .rd-doc .op-why a,
|
|
2174
2179
|
.atelic-ui .rd-beat a,
|
|
2175
2180
|
.atelic-ui .rd-gist-read a,
|
|
2176
2181
|
.atelic-ui .rd-plan a,
|
|
@@ -2249,22 +2254,6 @@
|
|
|
2249
2254
|
text-wrap: pretty;
|
|
2250
2255
|
min-width: 0;
|
|
2251
2256
|
}
|
|
2252
|
-
|
|
2253
|
-
/* ---- a section: the trade name over the question, then its beats ---- */
|
|
2254
|
-
.atelic-ui .rd-name {
|
|
2255
|
-
margin: 0 0 4px;
|
|
2256
|
-
font-family: var(--font-mono);
|
|
2257
|
-
font-size: 11.5px;
|
|
2258
|
-
letter-spacing: 0.1em;
|
|
2259
|
-
text-transform: uppercase;
|
|
2260
|
-
color: var(--op-ink-faint);
|
|
2261
|
-
}
|
|
2262
|
-
.atelic-ui .rd-grade .op-grade {
|
|
2263
|
-
width: 42px;
|
|
2264
|
-
height: 42px;
|
|
2265
|
-
border-radius: 12px;
|
|
2266
|
-
font-size: 21px;
|
|
2267
|
-
}
|
|
2268
2257
|
.atelic-ui .rd-beat {
|
|
2269
2258
|
margin-top: 20px;
|
|
2270
2259
|
min-width: 0;
|
|
@@ -2286,127 +2275,19 @@
|
|
|
2286
2275
|
.atelic-ui .rd-beat p:last-child {
|
|
2287
2276
|
margin-bottom: 0;
|
|
2288
2277
|
}
|
|
2289
|
-
/*
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
list-style: none;
|
|
2293
|
-
margin: 0;
|
|
2294
|
-
padding: 0;
|
|
2295
|
-
display: flex;
|
|
2296
|
-
flex-wrap: wrap;
|
|
2297
|
-
gap: 10px 28px;
|
|
2298
|
-
}
|
|
2299
|
-
.atelic-ui .rd-tools-list li {
|
|
2300
|
-
display: flex;
|
|
2301
|
-
flex-direction: column;
|
|
2302
|
-
gap: 1px;
|
|
2303
|
-
min-width: 0;
|
|
2304
|
-
max-width: 100%;
|
|
2305
|
-
}
|
|
2306
|
-
.atelic-ui .rd-tools-list a,
|
|
2307
|
-
.atelic-ui .rd-tool-name {
|
|
2308
|
-
display: inline-flex;
|
|
2309
|
-
align-items: center;
|
|
2310
|
-
gap: 3px;
|
|
2311
|
-
font-size: 15px;
|
|
2312
|
-
font-weight: 600;
|
|
2313
|
-
color: var(--op-accent);
|
|
2314
|
-
text-decoration: none;
|
|
2315
|
-
}
|
|
2316
|
-
.atelic-ui .rd-tool-name {
|
|
2317
|
-
color: var(--op-ink);
|
|
2318
|
-
}
|
|
2319
|
-
.atelic-ui .rd-tools-list a:hover {
|
|
2320
|
-
text-decoration: underline;
|
|
2321
|
-
text-underline-offset: 2px;
|
|
2322
|
-
}
|
|
2323
|
-
.atelic-ui .rd-tools-list svg {
|
|
2324
|
-
flex: none;
|
|
2325
|
-
}
|
|
2326
|
-
.atelic-ui .rd-tool-note {
|
|
2327
|
-
font-size: 13px;
|
|
2328
|
-
line-height: 1.35;
|
|
2329
|
-
color: var(--op-ink-faint);
|
|
2330
|
-
}
|
|
2331
|
-
|
|
2332
|
-
/* What it showed: the facts as a compact table of label and value, in
|
|
2333
|
-
the writeup's table dress (an ink rule over thin row rules). */
|
|
2334
|
-
.atelic-ui .rd-facts {
|
|
2335
|
-
width: 100%;
|
|
2336
|
-
max-width: 620px;
|
|
2337
|
-
margin: 12px 0 0;
|
|
2338
|
-
border-collapse: collapse;
|
|
2339
|
-
border-top: 1.5px solid var(--op-ink);
|
|
2340
|
-
font-size: 15px;
|
|
2341
|
-
line-height: 1.4;
|
|
2342
|
-
}
|
|
2343
|
-
.atelic-ui .rd-facts th,
|
|
2344
|
-
.atelic-ui .rd-facts td {
|
|
2345
|
-
padding: 7px 14px 7px 0;
|
|
2346
|
-
border-bottom: 1px solid var(--neutral-100);
|
|
2347
|
-
text-align: left;
|
|
2348
|
-
vertical-align: top;
|
|
2349
|
-
}
|
|
2350
|
-
.atelic-ui .rd-facts th {
|
|
2351
|
-
width: 55%;
|
|
2352
|
-
font-weight: 400;
|
|
2353
|
-
color: var(--op-ink-soft);
|
|
2354
|
-
}
|
|
2355
|
-
.atelic-ui .rd-facts td {
|
|
2356
|
-
padding-right: 0;
|
|
2357
|
-
font-weight: 600;
|
|
2358
|
-
color: var(--op-ink);
|
|
2359
|
-
font-variant-numeric: tabular-nums;
|
|
2360
|
-
}
|
|
2361
|
-
.atelic-ui .rd-facts tr:last-child > * {
|
|
2362
|
-
border-bottom: 0;
|
|
2363
|
-
}
|
|
2364
|
-
|
|
2365
|
-
/* The examples: a list or more, each under its own short heading. */
|
|
2366
|
-
.atelic-ui .rd-examples-block {
|
|
2367
|
-
margin-top: 14px;
|
|
2368
|
-
max-width: 68ch;
|
|
2369
|
-
}
|
|
2370
|
-
.atelic-ui .rd-examples-block h5 {
|
|
2371
|
-
margin: 0 0 4px;
|
|
2372
|
-
font-size: 15px;
|
|
2373
|
-
font-weight: 650;
|
|
2374
|
-
color: var(--op-ink);
|
|
2375
|
-
}
|
|
2376
|
-
.atelic-ui .rd-examples {
|
|
2377
|
-
list-style: disc;
|
|
2378
|
-
margin: 0;
|
|
2379
|
-
padding-left: 1.2em;
|
|
2380
|
-
color: var(--op-ink-soft);
|
|
2381
|
-
text-wrap: pretty;
|
|
2382
|
-
}
|
|
2383
|
-
.atelic-ui .rd-examples li + li {
|
|
2384
|
-
margin-top: 4px;
|
|
2385
|
-
}
|
|
2386
|
-
/* Why is one sentence, set apart: a tinted panel with the accent's rule. */
|
|
2387
|
-
.atelic-ui .rd-why {
|
|
2278
|
+
/* The note under the gist table: what the read leaves out, on a tinted panel with the accent's rule. */
|
|
2279
|
+
.atelic-ui .rd-note {
|
|
2280
|
+
margin-top: 18px;
|
|
2388
2281
|
padding: 12px 14px;
|
|
2389
2282
|
border-left: 3px solid var(--op-accent);
|
|
2390
2283
|
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
|
2391
2284
|
background: var(--surface-alt);
|
|
2392
2285
|
}
|
|
2393
|
-
.atelic-ui .rd-
|
|
2394
|
-
font-size:
|
|
2395
|
-
|
|
2396
|
-
line-height: 1.45;
|
|
2286
|
+
.atelic-ui .rd-note p {
|
|
2287
|
+
font-size: 15.5px;
|
|
2288
|
+
line-height: 1.5;
|
|
2397
2289
|
color: var(--op-ink);
|
|
2398
|
-
|
|
2399
|
-
/* The proof pictures sit in a row at one size, a search capture per
|
|
2400
|
-
market; a phone stacks them. */
|
|
2401
|
-
.atelic-ui .rd-shots {
|
|
2402
|
-
display: grid;
|
|
2403
|
-
grid-template-columns: minmax(0, 1fr);
|
|
2404
|
-
gap: 14px;
|
|
2405
|
-
margin-top: 14px;
|
|
2406
|
-
}
|
|
2407
|
-
/* A lone picture keeps a screen's size rather than filling the card. */
|
|
2408
|
-
.atelic-ui .rd-shots[data-count="1"] {
|
|
2409
|
-
max-width: 380px;
|
|
2290
|
+
text-wrap: pretty;
|
|
2410
2291
|
}
|
|
2411
2292
|
|
|
2412
2293
|
/* Today and after: what a thing is, and what it becomes. Two columns
|
|
@@ -2469,26 +2350,43 @@
|
|
|
2469
2350
|
color: var(--op-accent);
|
|
2470
2351
|
}
|
|
2471
2352
|
|
|
2472
|
-
/*
|
|
2473
|
-
.atelic-ui .rd-checks {
|
|
2474
|
-
|
|
2475
|
-
margin: 0;
|
|
2476
|
-
padding: 0;
|
|
2477
|
-
display: grid;
|
|
2478
|
-
gap: 8px;
|
|
2479
|
-
max-width: 68ch;
|
|
2353
|
+
/* The recommendations: ui's icon list across the card's width, each bullet the accent's check unless the item brings its own icon or tone. */
|
|
2354
|
+
.atelic-ui .rd-recommend .rd-checks {
|
|
2355
|
+
--tone: var(--op-accent);
|
|
2480
2356
|
}
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
column-gap: 10px;
|
|
2485
|
-
align-items: start;
|
|
2486
|
-
line-height: 1.45;
|
|
2487
|
-
text-wrap: pretty;
|
|
2357
|
+
/* A list or a table inside a finding: a little air above it. */
|
|
2358
|
+
.atelic-ui .op-findings .callout-body .icon-list {
|
|
2359
|
+
margin-top: 6px;
|
|
2488
2360
|
}
|
|
2489
|
-
.atelic-ui .
|
|
2490
|
-
|
|
2491
|
-
|
|
2361
|
+
.atelic-ui .op-findings .callout-body .data-table,
|
|
2362
|
+
.atelic-ui .rd-checks .data-table {
|
|
2363
|
+
margin-top: 12px;
|
|
2364
|
+
}
|
|
2365
|
+
/* In the read, three pictures fill the card's width. */
|
|
2366
|
+
.atelic-ui .rd-doc .op-shots-row[data-count="3"] {
|
|
2367
|
+
max-width: none;
|
|
2368
|
+
}
|
|
2369
|
+
/* Content patterns for a table inside a read, written by the content with ui's swatch: a rank with its badge in one cell, two equal halves, the swatch on the left and the figure on the right. These are the read's patterns, not a piece, until a consumer beyond the first wants them, when they move to ui. */
|
|
2370
|
+
.atelic-ui .rd-ranked {
|
|
2371
|
+
display: inline-grid;
|
|
2372
|
+
grid-template-columns: 1fr 1fr;
|
|
2373
|
+
column-gap: 8px;
|
|
2374
|
+
align-items: center;
|
|
2375
|
+
min-width: 3.4em;
|
|
2376
|
+
}
|
|
2377
|
+
.atelic-ui .rd-ranked .chart-swatch {
|
|
2378
|
+
justify-self: start;
|
|
2379
|
+
}
|
|
2380
|
+
.atelic-ui .rd-ranked > a,
|
|
2381
|
+
.atelic-ui .rd-ranked > span:last-child {
|
|
2382
|
+
justify-self: end;
|
|
2383
|
+
font-variant-numeric: tabular-nums;
|
|
2384
|
+
}
|
|
2385
|
+
/* A swatch as a table cell: the chart key's box, keying the row in its tone, in a column marked fit. */
|
|
2386
|
+
.atelic-ui .op-table .chart-swatch,
|
|
2387
|
+
.atelic-ui .op-findings .callout-body .chart-swatch {
|
|
2388
|
+
display: inline-block;
|
|
2389
|
+
vertical-align: middle;
|
|
2492
2390
|
}
|
|
2493
2391
|
|
|
2494
2392
|
/* ---- the plan: the steps in order, numbered ---- */
|
|
@@ -2583,9 +2481,6 @@
|
|
|
2583
2481
|
padding-left: 20px;
|
|
2584
2482
|
padding-right: 16px;
|
|
2585
2483
|
}
|
|
2586
|
-
.atelic-ui .rd-facts th {
|
|
2587
|
-
width: 45%;
|
|
2588
|
-
}
|
|
2589
2484
|
.atelic-ui .rd-changes-table {
|
|
2590
2485
|
display: table;
|
|
2591
2486
|
table-layout: fixed;
|
|
@@ -2646,12 +2541,6 @@
|
|
|
2646
2541
|
.atelic-ui .rd-changes-table tbody tr:last-child td {
|
|
2647
2542
|
border-bottom: 0;
|
|
2648
2543
|
}
|
|
2649
|
-
.atelic-ui .rd-shots[data-count="2"] {
|
|
2650
|
-
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
2651
|
-
}
|
|
2652
|
-
.atelic-ui .rd-shots[data-count="3"] {
|
|
2653
|
-
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
2654
|
-
}
|
|
2655
2544
|
}
|
|
2656
2545
|
|
|
2657
2546
|
/* ---- print: the writeup's rules carry the page; these hold the read's
|
|
@@ -2664,18 +2553,14 @@
|
|
|
2664
2553
|
}
|
|
2665
2554
|
.atelic-ui .rd-gist-table tr,
|
|
2666
2555
|
.atelic-ui .rd-changes-table tr,
|
|
2667
|
-
.atelic-ui .rd-facts,
|
|
2668
|
-
.atelic-ui .rd-tools,
|
|
2669
2556
|
.atelic-ui .rd-checks li,
|
|
2670
2557
|
.atelic-ui .rd-closing,
|
|
2671
|
-
.atelic-ui .rd-
|
|
2558
|
+
.atelic-ui .rd-note,
|
|
2672
2559
|
.atelic-ui .rd-step,
|
|
2673
|
-
.atelic-ui .rd-close
|
|
2674
|
-
.atelic-ui .rd-shots .op-shot {
|
|
2560
|
+
.atelic-ui .rd-close {
|
|
2675
2561
|
break-inside: avoid;
|
|
2676
2562
|
}
|
|
2677
2563
|
.atelic-ui .rd-beat h4,
|
|
2678
|
-
.atelic-ui .rd-examples-block h5,
|
|
2679
2564
|
.atelic-ui .rd-plan h2 {
|
|
2680
2565
|
break-after: avoid;
|
|
2681
2566
|
}
|