@atelic-action/artifact 1.3.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 +5 -4
- package/package.json +3 -3
- package/src/documents/Read.tsx +118 -202
- package/src/documents/Writeup.tsx +70 -19
- package/src/documents/documents.css +47 -178
package/README.md
CHANGED
|
@@ -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, with an optional `note` callout under it for what the read leaves out; each section
|
|
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,44 +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
|
-
* what
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
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.
|
|
34
35
|
*
|
|
35
|
-
* It
|
|
36
|
-
*
|
|
37
|
-
* (the `.
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* `
|
|
42
|
-
*
|
|
43
|
-
* the sign off person's `role`, which takes a node and is equally happy with
|
|
44
|
-
* 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.
|
|
45
44
|
*/
|
|
46
45
|
|
|
47
46
|
/** A link inside running text: the words the reader clicks and where they go. */
|
|
@@ -50,65 +49,36 @@ export type ReadLink = ArtifactLink;
|
|
|
50
49
|
/** A paragraph: a plain string, or runs of text and links in order. */
|
|
51
50
|
export type ReadParagraph = ArtifactParagraph;
|
|
52
51
|
|
|
53
|
-
/** A tool we looked through: its name, where it opens (in a new tab), and a line on what we read there. */
|
|
54
|
-
export interface ReadTool {
|
|
55
|
-
label: string;
|
|
56
|
-
href?: string;
|
|
57
|
-
/** One short line, e.g. "Page indexing, the last 90 days". */
|
|
58
|
-
note?: string;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** The 1.0.0 name for a tool; the same shape. */
|
|
62
|
-
export type ReadSource = ReadTool;
|
|
63
|
-
|
|
64
|
-
/** One row of a section's facts: a short label and its value, e.g. "Pages on Google" and "9 of 9". */
|
|
65
|
-
export interface ReadFact {
|
|
66
|
-
label: string;
|
|
67
|
-
value: ReadParagraph;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** A list of examples under its own short heading; each item a paragraph that can carry links. */
|
|
71
|
-
export interface ReadExamples {
|
|
72
|
-
title?: string;
|
|
73
|
-
items: ReadParagraph[];
|
|
74
|
-
}
|
|
75
|
-
|
|
76
52
|
/** One row of today and after: what a thing is now, and what it becomes. */
|
|
77
53
|
export interface ReadChange {
|
|
78
54
|
now: ReadParagraph;
|
|
79
55
|
after: ReadParagraph;
|
|
80
56
|
}
|
|
81
57
|
|
|
82
|
-
/**
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
/** The question the section answers, in the owner's words, e.g. "Can Google read the site?", set under the heading. */
|
|
89
|
-
question: string;
|
|
90
|
-
/** The baseline's grade. Omit for a section not yet graded; the table shows a quiet dash. */
|
|
91
|
-
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"> {
|
|
92
64
|
/** The one line in the gist table. */
|
|
93
65
|
gist: ReadParagraph;
|
|
94
|
-
/**
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
sources?: ReadTool[];
|
|
98
|
-
/** What it showed: one or two short paragraphs. */
|
|
99
|
-
found: ReadParagraph[];
|
|
100
|
-
/** What it showed, as a compact two column table of facts under the paragraphs. */
|
|
101
|
-
facts?: ReadFact[];
|
|
102
|
-
/** What it showed, as lists of examples, each under its own short heading. */
|
|
103
|
-
examples?: ReadExamples[];
|
|
104
|
-
/** Proof pictures, each over its caption, e.g. a search result capture per market. */
|
|
105
|
-
shots?: WriteupShot[];
|
|
106
|
-
/** Why, in one sentence, set apart. */
|
|
107
|
-
why: ReadParagraph;
|
|
108
|
-
/** 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. */
|
|
109
69
|
changes?: ReadChange[];
|
|
110
|
-
/**
|
|
111
|
-
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;
|
|
112
82
|
}
|
|
113
83
|
|
|
114
84
|
/** A tier, e.g. Health: its heading, the question it answers, and its sections in order. */
|
|
@@ -117,7 +87,7 @@ export interface ReadTier {
|
|
|
117
87
|
id?: string;
|
|
118
88
|
/** The tier's name, e.g. "Health". */
|
|
119
89
|
title: string;
|
|
120
|
-
/** The
|
|
90
|
+
/** The questions the tier answers, in the owner's words. */
|
|
121
91
|
lead?: string;
|
|
122
92
|
sections: ReadSection[];
|
|
123
93
|
}
|
|
@@ -161,18 +131,16 @@ export interface ReadLabels {
|
|
|
161
131
|
gist: string;
|
|
162
132
|
/** What a screen reader hears for a section with no grade. */
|
|
163
133
|
notGraded: string;
|
|
164
|
-
/** The beat at the top of a section: the tools we looked through. */
|
|
165
|
-
tools: string;
|
|
166
|
-
/** The beat that says what the tools showed. */
|
|
167
|
-
found: string;
|
|
168
|
-
/** The one sentence beat. */
|
|
169
|
-
why: string;
|
|
170
134
|
/** The today and after table's first column. */
|
|
171
135
|
now: string;
|
|
172
136
|
/** The today and after table's second column. */
|
|
173
137
|
after: string;
|
|
174
138
|
/** The checklist beat at the foot of a section. */
|
|
175
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;
|
|
176
144
|
/** The plan's heading when it names none. */
|
|
177
145
|
plan: string;
|
|
178
146
|
}
|
|
@@ -183,12 +151,11 @@ export const readLabels: ReadLabels = {
|
|
|
183
151
|
grade: "Grade",
|
|
184
152
|
gist: "The Gist",
|
|
185
153
|
notGraded: "Not graded yet",
|
|
186
|
-
tools: "Where We Looked",
|
|
187
|
-
found: "What It Showed",
|
|
188
|
-
why: "Why",
|
|
189
154
|
now: "Today",
|
|
190
155
|
after: "After",
|
|
191
|
-
recommend: "
|
|
156
|
+
recommend: "Recommendations",
|
|
157
|
+
why: "Why",
|
|
158
|
+
sources: "See it yourself",
|
|
192
159
|
plan: "The Plan",
|
|
193
160
|
};
|
|
194
161
|
|
|
@@ -218,51 +185,29 @@ function filled(paragraph: ReadParagraph | undefined | null): paragraph is ReadP
|
|
|
218
185
|
return paragraph !== undefined && paragraph !== null && !isEmpty(paragraph);
|
|
219
186
|
}
|
|
220
187
|
|
|
221
|
-
|
|
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;
|
|
222
191
|
return (
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
{tool.href ? (
|
|
230
|
-
<a href={tool.href} {...newTabProps(tool.href, true)}>
|
|
231
|
-
{tool.label}
|
|
232
|
-
<ArrowUpRight size={13} aria-hidden="true" />
|
|
233
|
-
</a>
|
|
234
|
-
) : (
|
|
235
|
-
<span className="rd-tool-name">{tool.label}</span>
|
|
236
|
-
)}
|
|
237
|
-
{tool.note && <span className="rd-tool-note">{tool.note}</span>}
|
|
238
|
-
</li>
|
|
239
|
-
))}
|
|
240
|
-
</ul>
|
|
241
|
-
</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
|
+
)
|
|
242
198
|
);
|
|
243
199
|
}
|
|
244
200
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
// biome-ignore lint/suspicious/noArrayIndexKey: facts are static content in a fixed order
|
|
251
|
-
<tr key={i}>
|
|
252
|
-
<th scope="row">{fact.label}</th>
|
|
253
|
-
<td>
|
|
254
|
-
<Runs paragraph={fact.value} />
|
|
255
|
-
</td>
|
|
256
|
-
</tr>
|
|
257
|
-
))}
|
|
258
|
-
</tbody>
|
|
259
|
-
</table>
|
|
260
|
-
);
|
|
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;
|
|
261
206
|
}
|
|
262
207
|
|
|
263
208
|
function Changes({ changes, labels }: { changes: ReadChange[]; labels: ReadLabels }) {
|
|
264
209
|
return (
|
|
265
|
-
<div className="rd-beat rd-changes">
|
|
210
|
+
<div className="op-block rd-beat rd-changes">
|
|
266
211
|
<table className="rd-changes-table">
|
|
267
212
|
<thead>
|
|
268
213
|
<tr>
|
|
@@ -292,21 +237,24 @@ function Changes({ changes, labels }: { changes: ReadChange[]; labels: ReadLabel
|
|
|
292
237
|
);
|
|
293
238
|
}
|
|
294
239
|
|
|
295
|
-
|
|
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 }) {
|
|
296
246
|
return (
|
|
297
|
-
<div className="rd-beat rd-recommend">
|
|
247
|
+
<div className="op-block rd-beat rd-recommend">
|
|
298
248
|
<h4>{label}</h4>
|
|
299
|
-
<
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
))}
|
|
309
|
-
</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
|
+
/>
|
|
310
258
|
</div>
|
|
311
259
|
);
|
|
312
260
|
}
|
|
@@ -365,7 +313,7 @@ function Gist({
|
|
|
365
313
|
{tier.sections.map((section) => (
|
|
366
314
|
<tr key={section.id}>
|
|
367
315
|
<th scope="row" className="rd-gist-section">
|
|
368
|
-
<a href={`#${section.id}`}>{section.
|
|
316
|
+
<a href={`#${section.id}`}>{section.title}</a>
|
|
369
317
|
</th>
|
|
370
318
|
<td className="rd-gist-grade">
|
|
371
319
|
{section.grade ? (
|
|
@@ -395,77 +343,45 @@ function Gist({
|
|
|
395
343
|
);
|
|
396
344
|
}
|
|
397
345
|
|
|
346
|
+
/** A section: the writeup's card, with today and after and the checklist handed in after its blocks. */
|
|
398
347
|
function Section({ section, labels }: { section: ReadSection; labels: ReadLabels }) {
|
|
399
|
-
const tools = (section.tools ?? section.sources ?? []).filter((tool) => tool?.label?.trim());
|
|
400
|
-
const found = (section.found ?? []).filter(filled);
|
|
401
|
-
const facts = (section.facts ?? []).filter((fact) => filled(fact?.value));
|
|
402
|
-
const examples = (section.examples ?? [])
|
|
403
|
-
.map((list) => ({ title: list?.title, items: (list?.items ?? []).filter(filled) }))
|
|
404
|
-
.filter((list) => list.items.length > 0);
|
|
405
|
-
const shots = section.shots ?? [];
|
|
406
348
|
const changes = (section.changes ?? []).filter(
|
|
407
349
|
(change) => filled(change?.now) || filled(change?.after),
|
|
408
350
|
);
|
|
409
|
-
const recommend = (section.recommend ?? [])
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
<GradeChip grade={section.grade} />
|
|
423
|
-
</span>
|
|
424
|
-
</>
|
|
425
|
-
)}
|
|
426
|
-
</header>
|
|
427
|
-
{tools.length > 0 && <Tools tools={tools} label={labels.tools} />}
|
|
428
|
-
{showed && (
|
|
429
|
-
<div className="rd-beat rd-found">
|
|
430
|
-
<h4>{labels.found}</h4>
|
|
431
|
-
{found.map((paragraph, i) => (
|
|
432
|
-
// biome-ignore lint/suspicious/noArrayIndexKey: paragraphs are static content in a fixed order
|
|
433
|
-
<Paragraph paragraph={paragraph} key={i} />
|
|
434
|
-
))}
|
|
435
|
-
{facts.length > 0 && <Facts facts={facts} />}
|
|
436
|
-
{examples.map((list, i) => (
|
|
437
|
-
// biome-ignore lint/suspicious/noArrayIndexKey: lists are static content in a fixed order, and a title may repeat
|
|
438
|
-
<div className="rd-examples-block" key={i}>
|
|
439
|
-
{list.title && <h5>{list.title}</h5>}
|
|
440
|
-
<ul className="rd-examples">
|
|
441
|
-
{list.items.map((example, j) => (
|
|
442
|
-
// biome-ignore lint/suspicious/noArrayIndexKey: examples are static content in a fixed order
|
|
443
|
-
<li key={j}>
|
|
444
|
-
<Runs paragraph={example} />
|
|
445
|
-
</li>
|
|
446
|
-
))}
|
|
447
|
-
</ul>
|
|
448
|
-
</div>
|
|
449
|
-
))}
|
|
450
|
-
{shots.length > 0 && (
|
|
451
|
-
<div className="rd-shots" data-count={Math.min(shots.length, 3)}>
|
|
452
|
-
{shots.map((shot) => (
|
|
453
|
-
<Shot shot={shot} key={shot.src} />
|
|
454
|
-
))}
|
|
455
|
-
</div>
|
|
456
|
-
)}
|
|
457
|
-
</div>
|
|
458
|
-
)}
|
|
459
|
-
{filled(section.why) && (
|
|
460
|
-
<div className="rd-beat rd-why">
|
|
461
|
-
<h4>{labels.why}</h4>
|
|
462
|
-
<Paragraph paragraph={section.why} />
|
|
463
|
-
</div>
|
|
464
|
-
)}
|
|
465
|
-
{changes.length > 0 && <Changes changes={changes} labels={labels} />}
|
|
466
|
-
{recommend.length > 0 && <Recommend items={recommend} label={labels.recommend} />}
|
|
467
|
-
</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,
|
|
468
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} />;
|
|
469
385
|
}
|
|
470
386
|
|
|
471
387
|
function Plan({ plan, labels }: { plan: ReadPlan; labels: ReadLabels }) {
|
|
@@ -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,18 +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,
|
|
2174
2177
|
.atelic-ui .rd-note a,
|
|
2178
|
+
.atelic-ui .rd-doc .op-why a,
|
|
2175
2179
|
.atelic-ui .rd-beat a,
|
|
2176
2180
|
.atelic-ui .rd-gist-read a,
|
|
2177
2181
|
.atelic-ui .rd-plan a,
|
|
@@ -2250,22 +2254,6 @@
|
|
|
2250
2254
|
text-wrap: pretty;
|
|
2251
2255
|
min-width: 0;
|
|
2252
2256
|
}
|
|
2253
|
-
|
|
2254
|
-
/* ---- a section: the trade name over the question, then its beats ---- */
|
|
2255
|
-
/* The owner's question sits under the section's name. */
|
|
2256
|
-
.atelic-ui .rd-question {
|
|
2257
|
-
margin: 4px 0 0;
|
|
2258
|
-
font-size: 15.5px;
|
|
2259
|
-
line-height: 1.4;
|
|
2260
|
-
color: var(--op-ink-soft);
|
|
2261
|
-
text-wrap: pretty;
|
|
2262
|
-
}
|
|
2263
|
-
.atelic-ui .rd-grade .op-grade {
|
|
2264
|
-
width: 42px;
|
|
2265
|
-
height: 42px;
|
|
2266
|
-
border-radius: 12px;
|
|
2267
|
-
font-size: 21px;
|
|
2268
|
-
}
|
|
2269
2257
|
.atelic-ui .rd-beat {
|
|
2270
2258
|
margin-top: 20px;
|
|
2271
2259
|
min-width: 0;
|
|
@@ -2287,104 +2275,7 @@
|
|
|
2287
2275
|
.atelic-ui .rd-beat p:last-child {
|
|
2288
2276
|
margin-bottom: 0;
|
|
2289
2277
|
}
|
|
2290
|
-
/*
|
|
2291
|
-
link with its line under it. */
|
|
2292
|
-
.atelic-ui .rd-tools-list {
|
|
2293
|
-
list-style: none;
|
|
2294
|
-
margin: 0;
|
|
2295
|
-
padding: 0;
|
|
2296
|
-
display: flex;
|
|
2297
|
-
flex-wrap: wrap;
|
|
2298
|
-
gap: 10px 28px;
|
|
2299
|
-
}
|
|
2300
|
-
.atelic-ui .rd-tools-list li {
|
|
2301
|
-
display: flex;
|
|
2302
|
-
flex-direction: column;
|
|
2303
|
-
gap: 1px;
|
|
2304
|
-
min-width: 0;
|
|
2305
|
-
max-width: 100%;
|
|
2306
|
-
}
|
|
2307
|
-
.atelic-ui .rd-tools-list a,
|
|
2308
|
-
.atelic-ui .rd-tool-name {
|
|
2309
|
-
display: inline-flex;
|
|
2310
|
-
align-items: center;
|
|
2311
|
-
gap: 3px;
|
|
2312
|
-
font-size: 15px;
|
|
2313
|
-
font-weight: 600;
|
|
2314
|
-
color: var(--op-accent);
|
|
2315
|
-
text-decoration: none;
|
|
2316
|
-
}
|
|
2317
|
-
.atelic-ui .rd-tool-name {
|
|
2318
|
-
color: var(--op-ink);
|
|
2319
|
-
}
|
|
2320
|
-
.atelic-ui .rd-tools-list a:hover {
|
|
2321
|
-
text-decoration: underline;
|
|
2322
|
-
text-underline-offset: 2px;
|
|
2323
|
-
}
|
|
2324
|
-
.atelic-ui .rd-tools-list svg {
|
|
2325
|
-
flex: none;
|
|
2326
|
-
}
|
|
2327
|
-
.atelic-ui .rd-tool-note {
|
|
2328
|
-
font-size: 13px;
|
|
2329
|
-
line-height: 1.35;
|
|
2330
|
-
color: var(--op-ink-faint);
|
|
2331
|
-
}
|
|
2332
|
-
|
|
2333
|
-
/* What it showed: the facts as a compact table of label and value, in
|
|
2334
|
-
the writeup's table dress (an ink rule over thin row rules). */
|
|
2335
|
-
.atelic-ui .rd-facts {
|
|
2336
|
-
width: 100%;
|
|
2337
|
-
max-width: 620px;
|
|
2338
|
-
margin: 12px 0 0;
|
|
2339
|
-
border-collapse: collapse;
|
|
2340
|
-
border-top: 1.5px solid var(--op-ink);
|
|
2341
|
-
font-size: 15px;
|
|
2342
|
-
line-height: 1.4;
|
|
2343
|
-
}
|
|
2344
|
-
.atelic-ui .rd-facts th,
|
|
2345
|
-
.atelic-ui .rd-facts td {
|
|
2346
|
-
padding: 7px 14px 7px 0;
|
|
2347
|
-
border-bottom: 1px solid var(--neutral-100);
|
|
2348
|
-
text-align: left;
|
|
2349
|
-
vertical-align: top;
|
|
2350
|
-
}
|
|
2351
|
-
.atelic-ui .rd-facts th {
|
|
2352
|
-
width: 55%;
|
|
2353
|
-
font-weight: 400;
|
|
2354
|
-
color: var(--op-ink-soft);
|
|
2355
|
-
}
|
|
2356
|
-
.atelic-ui .rd-facts td {
|
|
2357
|
-
padding-right: 0;
|
|
2358
|
-
font-weight: 600;
|
|
2359
|
-
color: var(--op-ink);
|
|
2360
|
-
font-variant-numeric: tabular-nums;
|
|
2361
|
-
}
|
|
2362
|
-
.atelic-ui .rd-facts tr:last-child > * {
|
|
2363
|
-
border-bottom: 0;
|
|
2364
|
-
}
|
|
2365
|
-
|
|
2366
|
-
/* The examples: a list or more, each under its own short heading. */
|
|
2367
|
-
.atelic-ui .rd-examples-block {
|
|
2368
|
-
margin-top: 14px;
|
|
2369
|
-
max-width: 68ch;
|
|
2370
|
-
}
|
|
2371
|
-
.atelic-ui .rd-examples-block h5 {
|
|
2372
|
-
margin: 0 0 4px;
|
|
2373
|
-
font-size: 15px;
|
|
2374
|
-
font-weight: 650;
|
|
2375
|
-
color: var(--op-ink);
|
|
2376
|
-
}
|
|
2377
|
-
.atelic-ui .rd-examples {
|
|
2378
|
-
list-style: disc;
|
|
2379
|
-
margin: 0;
|
|
2380
|
-
padding-left: 1.2em;
|
|
2381
|
-
color: var(--op-ink-soft);
|
|
2382
|
-
text-wrap: pretty;
|
|
2383
|
-
}
|
|
2384
|
-
.atelic-ui .rd-examples li + li {
|
|
2385
|
-
margin-top: 4px;
|
|
2386
|
-
}
|
|
2387
|
-
/* The note under the gist table: what the read leaves out, on the why panel's tint. */
|
|
2278
|
+
/* The note under the gist table: what the read leaves out, on a tinted panel with the accent's rule. */
|
|
2388
2279
|
.atelic-ui .rd-note {
|
|
2389
2280
|
margin-top: 18px;
|
|
2390
2281
|
padding: 12px 14px;
|
|
@@ -2398,31 +2289,6 @@
|
|
|
2398
2289
|
color: var(--op-ink);
|
|
2399
2290
|
text-wrap: pretty;
|
|
2400
2291
|
}
|
|
2401
|
-
/* Why is one sentence, set apart: a tinted panel with the accent's rule. */
|
|
2402
|
-
.atelic-ui .rd-why {
|
|
2403
|
-
padding: 12px 14px;
|
|
2404
|
-
border-left: 3px solid var(--op-accent);
|
|
2405
|
-
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
|
2406
|
-
background: var(--surface-alt);
|
|
2407
|
-
}
|
|
2408
|
-
.atelic-ui .rd-why p {
|
|
2409
|
-
font-size: 16.5px;
|
|
2410
|
-
font-weight: 550;
|
|
2411
|
-
line-height: 1.45;
|
|
2412
|
-
color: var(--op-ink);
|
|
2413
|
-
}
|
|
2414
|
-
/* The proof pictures sit in a row at one size, a search capture per
|
|
2415
|
-
market; a phone stacks them. */
|
|
2416
|
-
.atelic-ui .rd-shots {
|
|
2417
|
-
display: grid;
|
|
2418
|
-
grid-template-columns: minmax(0, 1fr);
|
|
2419
|
-
gap: 14px;
|
|
2420
|
-
margin-top: 14px;
|
|
2421
|
-
}
|
|
2422
|
-
/* A lone picture keeps a screen's size rather than filling the card. */
|
|
2423
|
-
.atelic-ui .rd-shots[data-count="1"] {
|
|
2424
|
-
max-width: 380px;
|
|
2425
|
-
}
|
|
2426
2292
|
|
|
2427
2293
|
/* Today and after: what a thing is, and what it becomes. Two columns
|
|
2428
2294
|
wide; on a phone each row stacks, each half wearing its label. */
|
|
@@ -2484,26 +2350,43 @@
|
|
|
2484
2350
|
color: var(--op-accent);
|
|
2485
2351
|
}
|
|
2486
2352
|
|
|
2487
|
-
/*
|
|
2488
|
-
.atelic-ui .rd-checks {
|
|
2489
|
-
|
|
2490
|
-
margin: 0;
|
|
2491
|
-
padding: 0;
|
|
2492
|
-
display: grid;
|
|
2493
|
-
gap: 8px;
|
|
2494
|
-
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);
|
|
2495
2356
|
}
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
column-gap: 10px;
|
|
2500
|
-
align-items: start;
|
|
2501
|
-
line-height: 1.45;
|
|
2502
|
-
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;
|
|
2503
2360
|
}
|
|
2504
|
-
.atelic-ui .
|
|
2505
|
-
|
|
2506
|
-
|
|
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;
|
|
2507
2390
|
}
|
|
2508
2391
|
|
|
2509
2392
|
/* ---- the plan: the steps in order, numbered ---- */
|
|
@@ -2598,9 +2481,6 @@
|
|
|
2598
2481
|
padding-left: 20px;
|
|
2599
2482
|
padding-right: 16px;
|
|
2600
2483
|
}
|
|
2601
|
-
.atelic-ui .rd-facts th {
|
|
2602
|
-
width: 45%;
|
|
2603
|
-
}
|
|
2604
2484
|
.atelic-ui .rd-changes-table {
|
|
2605
2485
|
display: table;
|
|
2606
2486
|
table-layout: fixed;
|
|
@@ -2661,12 +2541,6 @@
|
|
|
2661
2541
|
.atelic-ui .rd-changes-table tbody tr:last-child td {
|
|
2662
2542
|
border-bottom: 0;
|
|
2663
2543
|
}
|
|
2664
|
-
.atelic-ui .rd-shots[data-count="2"] {
|
|
2665
|
-
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
2666
|
-
}
|
|
2667
|
-
.atelic-ui .rd-shots[data-count="3"] {
|
|
2668
|
-
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
2669
|
-
}
|
|
2670
2544
|
}
|
|
2671
2545
|
|
|
2672
2546
|
/* ---- print: the writeup's rules carry the page; these hold the read's
|
|
@@ -2679,19 +2553,14 @@
|
|
|
2679
2553
|
}
|
|
2680
2554
|
.atelic-ui .rd-gist-table tr,
|
|
2681
2555
|
.atelic-ui .rd-changes-table tr,
|
|
2682
|
-
.atelic-ui .rd-facts,
|
|
2683
|
-
.atelic-ui .rd-tools,
|
|
2684
2556
|
.atelic-ui .rd-checks li,
|
|
2685
2557
|
.atelic-ui .rd-closing,
|
|
2686
2558
|
.atelic-ui .rd-note,
|
|
2687
|
-
.atelic-ui .rd-why,
|
|
2688
2559
|
.atelic-ui .rd-step,
|
|
2689
|
-
.atelic-ui .rd-close
|
|
2690
|
-
.atelic-ui .rd-shots .op-shot {
|
|
2560
|
+
.atelic-ui .rd-close {
|
|
2691
2561
|
break-inside: avoid;
|
|
2692
2562
|
}
|
|
2693
2563
|
.atelic-ui .rd-beat h4,
|
|
2694
|
-
.atelic-ui .rd-examples-block h5,
|
|
2695
2564
|
.atelic-ui .rd-plan h2 {
|
|
2696
2565
|
break-after: avoid;
|
|
2697
2566
|
}
|