@atelic-action/artifact 1.0.0 → 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 CHANGED
@@ -25,9 +25,9 @@ Every export of the package, all from the one entry, `@atelic-action/artifact`.
25
25
  | `ratingCell` | A star rating table cell for a writeup table, sorted by its figure | `/writeup` |
26
26
  | `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
27
  | `WriteupGrade`, `WriteupPerson`, `WriteupShot` | The grade letter, the sign off person, and the owner's screen, shared by every document (the first two are re exported from ui) | none |
28
- | `Read`, `ReadProps` | The baseline's grades explained by cause: the gist table, each section in three beats, the plan in order | `/read` |
29
- | `readLabels`, `ReadLabels` | The fixed words the read sets (the gist table's columns, the three beats, the plan's heading), each overridable through `labels` | `/read` |
30
- | `ReadTier`, `ReadSection`, `ReadStep`, `ReadPlan`, `ReadClose`, `ReadSource`, `ReadLink`, `ReadParagraph` | The shapes a read's content is written in | none |
28
+ | `Read`, `ReadProps` | The baseline's grades explained by cause: the gist table; each section read top to bottom as where we looked (the tools, linked, at the top), what it showed (a short paragraph or two, a compact table of facts, lists of examples under their own headings, and any proof pictures), why (one sentence on a tinted panel), 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, heading and all | `/read` |
29
+ | `readLabels`, `ReadLabels` | The fixed words the read sets (Prepared for, the gist table's columns, Where We Looked, What It Showed, Why, Today, After, What We Recommend, the plan's heading), each overridable through `labels` | `/read` |
30
+ | `ReadTier`, `ReadSection`, `ReadTool`, `ReadFact`, `ReadExamples`, `ReadChange`, `ReadStep`, `ReadPlan`, `ReadClose`, `ReadLink`, `ReadParagraph` | The shapes a read's content is written in, all plain data so a routine can write a read as JSON. A section's `tools` were its `sources` in 1.0.0; `sources` is still read, and rendered at the top as tools, and `ReadSource` names the same shape as `ReadTool` | none |
31
31
  | `Report`, `ReportProps` | The monthly page: the four questions, the seven grades now and before, the sign off, on one letter sheet | `/report` |
32
32
  | `ReportLead`, `ReportLeads`, `ReportGradeRow` | The shapes a monthly page's content is written in | none |
33
33
  | `Note`, `NoteProps` | The first touch set as a page: the opener, two lanes of findings, where the work would start, the close | `/notes` |
@@ -89,7 +89,7 @@ No environment variables required. Visit [http://localhost:5173](http://localhos
89
89
 
90
90
  ## Building a Client Artifact
91
91
 
92
- A business's artifacts live in one folder of `atelic-action/writeups`, named by the business's short name and served at `<short>.atelic.me` as its own Vercel project with the folder as its root directory, one path per artifact. That host is the business's one address with the practice: the website build, when one is previewed, sits on it at `/proto`, proxied from its own project ([DEPLOY.md](DEPLOY.md), The Website Build Under /proto). Cut it from this one with `git subtree add --prefix=<short> https://github.com/atelic-action/artifact.git main`, run in `writeups`. A cut is a site, not the package, so it then takes the package from npm instead of carrying its source: delete `src/documents/` and `tst/documents/`, give `package.json` the folder's own name and drop the package's fields (`version`, `files`, `exports`, `sideEffects`, `publishConfig`, `peerDependencies`), and run `bun add @atelic-action/artifact`. The routes and content already import the documents by the package's name, so nothing else changes. Nothing is pulled forward from here afterward: a document reaches the folder with `bun update @atelic-action/artifact`, and everything else with logic in it is `@atelic-action/ui`'s and arrives with `bun update @atelic-action/ui` ([CLAUDE.md](CLAUDE.md), What a Cut Takes From Here). Delete the artifacts the engagement does not need (the route, the content module, and the prerender entry), then touch four surfaces:
92
+ A business's artifacts live in one folder of `atelic-action/writeups`, named by the business's short name and served at `<short>.atelic.me` as its own Vercel project with the folder as its root directory, one path per artifact. That host is the business's one address with the practice: the website build, when one is previewed, sits on it at `/proto`, proxied from its own project ([DEPLOY.md](DEPLOY.md), The Website Build Under /proto). Cut it from this one with `git subtree add --prefix=<short> https://github.com/atelic-action/artifact.git main`, run in `writeups`. A cut is a site, not the package, so it then takes the package from npm instead of carrying its source: delete `src/documents/` and `tst/documents/`, give `package.json` the folder's own name and drop the package's fields (`version`, `files`, `exports`, `sideEffects`, `peerDependencies`), and run `bun add @atelic-action/artifact`. The routes and content already import the documents by the package's name, so nothing else changes. Nothing is pulled forward from here afterward: a document reaches the folder with `bun update @atelic-action/artifact`, and everything else with logic in it is `@atelic-action/ui`'s and arrives with `bun update @atelic-action/ui` ([CLAUDE.md](CLAUDE.md), What a Cut Takes From Here). Delete the artifacts the engagement does not need (the route, the content module, and the prerender entry), then touch four surfaces:
93
93
 
94
94
  1. **Content**, in `src/content/`. One module per artifact: the real read, every number dated and defensible in the room, graded and shaped by the practice's `FOOTPRINT.md` and written to its pitch surface rules (`Brand/voice.md`, Pitch Surfaces 15 to 21). In `writeup.tsx`, `sheets[]` only groups the blocks; print flows the page across as many letter pages as it needs, and `writeupStops` names the header's stops.
95
95
  2. **Images**, split by what they are. Logos are brand marks and go in `public/brand/` (the recipient's logo, e.g. `public/brand/<name>.svg`), which the access wall leaves open so the header and the login page can draw them. The owner's own screens (the full screenshot for the click through and a tight crop for the column) and the QR for the twin, pointing at the bare host (`qr "https://<short>.atelic.me" > public/images/qr.png` with the `qrcode` package), go in `public/images/`, which the wall holds on a private artifact. A cut made before this split moves its recipient logo from `public/images/` to `public/brand/` and repoints `recipient.logo.src`.
@@ -149,7 +149,6 @@ A document is fixed or grown here, in `src/documents/`, with its test in `tst/do
149
149
 
150
150
  Two things differ from ui. A Trusted Publisher is set on a package that already exists, so the first version, 1.0.0, is published by hand by an owner of the `@atelic-action` scope (`npm publish --access public` from a clean checkout of the tag) before the Trusted Publisher is added and the workflow takes over. And this repo is private, so npm attaches no provenance to what it publishes; the package's source is public on npm all the same, which is why `files` ships only `src/documents/` (README.md and package.json ride along, as npm always includes them), and why nothing in them may name a client.
151
151
 
152
-
153
152
  ## License
154
153
 
155
154
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atelic-action/artifact",
3
- "version": "1.0.0",
3
+ "version": "1.1.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",
@@ -8,9 +8,6 @@
8
8
  "type": "git",
9
9
  "url": "git+https://github.com/atelic-action/artifact.git"
10
10
  },
11
- "publishConfig": {
12
- "access": "public"
13
- },
14
11
  "files": [
15
12
  "src/documents"
16
13
  ],
@@ -6,7 +6,7 @@ import {
6
6
  type WriteupGrade,
7
7
  type WriteupPerson,
8
8
  } from "@atelic-action/ui/artifact";
9
- import { ArrowUpRight } from "lucide-react";
9
+ import { ArrowUpRight, Check } from "lucide-react";
10
10
  import { type ArtifactLink, type ArtifactParagraph, isEmpty, Paragraph, Runs } from "./Paragraph";
11
11
  import { Shot, type WriteupShot } from "./Shot";
12
12
 
@@ -15,20 +15,26 @@ import { Shot, type WriteupShot } from "./Shot";
15
15
  * before anything changes. Where the writeup says what letter each section
16
16
  * earned, the read says why, and what changes. Its shape is the practice's
17
17
  * FOOTPRINT.md, under What the Owner Gets: a title and a dated lede; the gist
18
- * table (every section, its grade, and one line); then the tiers, each
19
- * section a question in three beats (what we found, why, what changes) with
20
- * any proof pictures and the tools it was read from; then the plan, in order;
21
- * then one closing line and the sign off.
18
+ * table (every section, its grade, and one line); then the tiers; then the
19
+ * plan, in order; then one closing line and the sign off.
20
+ *
21
+ * Each section reads as an aggregator of the tools we looked through, top to
22
+ * bottom: where we looked (the tools, linked, each with an optional line);
23
+ * what it showed (a short paragraph or two, a compact table of facts, lists
24
+ * of examples, and any proof pictures); why, in one sentence on a tinted
25
+ * panel; today and after, a two column table of what a thing is and what it
26
+ * becomes; and what we recommend, as a checklist. A beat with nothing in it
27
+ * is left out, heading and all.
22
28
  *
23
29
  * It is the writeup's sibling, not a variant: it wears the writeup's column,
24
- * head, grade chip and key, tier heading, section card, screen, sources, and
25
- * close (the `.op` classes), and adds only what is its own (the `.rd`
26
- * classes). The gist table is drawn from the tiers, so a tier added later
27
- * (Conversion, once the analytics access lands) is one more entry in `tiers`
28
- * and appears in the table and the body alike, with no change to the props.
29
- * Every prop is plain data, so a routine can write a read as JSON; the one
30
- * exception is the sign off person's `role`, which takes a node and is
31
- * equally happy with a string.
30
+ * head, grade chip and key, tier heading, section card, screen, and close
31
+ * (the `.op` classes), and adds only what is its own (the `.rd` classes).
32
+ * The gist table is drawn from the tiers, so a tier added later (Conversion,
33
+ * once the analytics access lands) is one more entry in `tiers` and appears
34
+ * in the table and the body alike, with no change to the props. Every prop
35
+ * is plain data, so a routine can write a read as JSON; the one exception is
36
+ * the sign off person's `role`, which takes a node and is equally happy with
37
+ * a string.
32
38
  */
33
39
 
34
40
  /** A link inside running text: the words the reader clicks and where they go. */
@@ -37,13 +43,36 @@ export type ReadLink = ArtifactLink;
37
43
  /** A paragraph: a plain string, or runs of text and links in order. */
38
44
  export type ReadParagraph = ArtifactParagraph;
39
45
 
40
- /** A place a reading came from: the tool, opened in a new tab when it has an address. */
41
- export interface ReadSource {
46
+ /** A tool we looked through: its name, where it opens (in a new tab), and a line on what we read there. */
47
+ export interface ReadTool {
42
48
  label: string;
43
49
  href?: string;
50
+ /** One short line, e.g. "Page indexing, the last 90 days". */
51
+ note?: string;
52
+ }
53
+
54
+ /** The 1.0.0 name for a tool; the same shape. */
55
+ export type ReadSource = ReadTool;
56
+
57
+ /** One row of a section's facts: a short label and its value, e.g. "Pages on Google" and "9 of 9". */
58
+ export interface ReadFact {
59
+ label: string;
60
+ value: ReadParagraph;
61
+ }
62
+
63
+ /** A list of examples under its own short heading; each item a paragraph that can carry links. */
64
+ export interface ReadExamples {
65
+ title?: string;
66
+ items: ReadParagraph[];
67
+ }
68
+
69
+ /** One row of today and after: what a thing is now, and what it becomes. */
70
+ export interface ReadChange {
71
+ now: ReadParagraph;
72
+ after: ReadParagraph;
44
73
  }
45
74
 
46
- /** One section of a tier: a question in three beats. */
75
+ /** One section of a tier: a question, read top to bottom in five beats. */
47
76
  export interface ReadSection {
48
77
  /** The anchor the gist table links to, e.g. "technical-seo". */
49
78
  id: string;
@@ -55,18 +84,24 @@ export interface ReadSection {
55
84
  grade?: WriteupGrade;
56
85
  /** The one line in the gist table. */
57
86
  gist: ReadParagraph;
58
- /** What we found: one paragraph or more. */
87
+ /** Where we looked: the tools the readings came from, linked, at the top of the section. */
88
+ tools?: ReadTool[];
89
+ /** The 1.0.0 name for `tools`, still read and rendered in the same place; `tools` wins when both are set. */
90
+ sources?: ReadTool[];
91
+ /** What it showed: one or two short paragraphs. */
59
92
  found: ReadParagraph[];
60
- /** A few concrete examples of what was found, as a short list under it. */
61
- examples?: ReadParagraph[];
93
+ /** What it showed, as a compact two column table of facts under the paragraphs. */
94
+ facts?: ReadFact[];
95
+ /** What it showed, as lists of examples, each under its own short heading. */
96
+ examples?: ReadExamples[];
62
97
  /** Proof pictures, each over its caption, e.g. a search result capture per market. */
63
98
  shots?: WriteupShot[];
64
99
  /** Why, in one sentence, set apart. */
65
100
  why: ReadParagraph;
66
- /** What changes: one paragraph or more. */
67
- changes: ReadParagraph[];
68
- /** The tools the readings came from, linked, so the owner can open them. */
69
- sources?: ReadSource[];
101
+ /** Today and after: what each thing is, and what it becomes. */
102
+ changes?: ReadChange[];
103
+ /** What we recommend, as a checklist. */
104
+ recommend?: ReadParagraph[];
70
105
  }
71
106
 
72
107
  /** A tier, e.g. Health: its heading, the question it answers, and its sections in order. */
@@ -109,6 +144,8 @@ export interface ReadClose {
109
144
 
110
145
  /** The fixed words the page sets, each overridable. */
111
146
  export interface ReadLabels {
147
+ /** The dateline's words before the business, e.g. "Prepared for". */
148
+ preparedFor: string;
112
149
  /** The gist table's first column. */
113
150
  section: string;
114
151
  /** The gist table's second column. */
@@ -117,24 +154,34 @@ export interface ReadLabels {
117
154
  gist: string;
118
155
  /** What a screen reader hears for a section with no grade. */
119
156
  notGraded: string;
157
+ /** The beat at the top of a section: the tools we looked through. */
158
+ tools: string;
159
+ /** The beat that says what the tools showed. */
120
160
  found: string;
161
+ /** The one sentence beat. */
121
162
  why: string;
122
- changes: string;
123
- /** The label over a section's tools. */
124
- sources: string;
163
+ /** The today and after table's first column. */
164
+ now: string;
165
+ /** The today and after table's second column. */
166
+ after: string;
167
+ /** The checklist beat at the foot of a section. */
168
+ recommend: string;
125
169
  /** The plan's heading when it names none. */
126
170
  plan: string;
127
171
  }
128
172
 
129
173
  export const readLabels: ReadLabels = {
174
+ preparedFor: "Prepared for",
130
175
  section: "Section",
131
176
  grade: "Grade",
132
177
  gist: "The Gist",
133
178
  notGraded: "Not graded yet",
134
- found: "What We Found",
179
+ tools: "Where We Looked",
180
+ found: "What It Showed",
135
181
  why: "Why",
136
- changes: "What Changes",
137
- sources: "Sources",
182
+ now: "Today",
183
+ after: "After",
184
+ recommend: "What We Recommend",
138
185
  plan: "The Plan",
139
186
  };
140
187
 
@@ -157,25 +204,101 @@ export interface ReadProps {
157
204
  labels?: Partial<ReadLabels>;
158
205
  }
159
206
 
160
- function Sources({ sources, label }: { sources: ReadSource[]; label: string }) {
207
+ /** A paragraph with something to read. Content may arrive as JSON, so a missing one counts as empty. */
208
+ function filled(paragraph: ReadParagraph | undefined | null): paragraph is ReadParagraph {
209
+ return paragraph !== undefined && paragraph !== null && !isEmpty(paragraph);
210
+ }
211
+
212
+ function Tools({ tools, label }: { tools: ReadTool[]; label: string }) {
161
213
  return (
162
- <footer className="op-sources">
163
- <span className="op-sources-label">{label}</span>
164
- <ul>
165
- {sources.map((source) => (
166
- <li key={source.label}>
167
- {source.href ? (
168
- <a href={source.href} {...newTabProps(source.href, true)}>
169
- {source.label}
170
- <ArrowUpRight size={12} aria-hidden="true" />
214
+ <div className="rd-beat rd-tools">
215
+ <h4>{label}</h4>
216
+ <ul className="rd-tools-list">
217
+ {tools.map((tool, i) => (
218
+ // biome-ignore lint/suspicious/noArrayIndexKey: tools are static content in a fixed order, and a label may repeat
219
+ <li key={i}>
220
+ {tool.href ? (
221
+ <a href={tool.href} {...newTabProps(tool.href, true)}>
222
+ {tool.label}
223
+ <ArrowUpRight size={13} aria-hidden="true" />
171
224
  </a>
172
225
  ) : (
173
- source.label
226
+ <span className="rd-tool-name">{tool.label}</span>
174
227
  )}
228
+ {tool.note && <span className="rd-tool-note">{tool.note}</span>}
229
+ </li>
230
+ ))}
231
+ </ul>
232
+ </div>
233
+ );
234
+ }
235
+
236
+ function Facts({ facts }: { facts: ReadFact[] }) {
237
+ return (
238
+ <table className="rd-facts">
239
+ <tbody>
240
+ {facts.map((fact, i) => (
241
+ // biome-ignore lint/suspicious/noArrayIndexKey: facts are static content in a fixed order
242
+ <tr key={i}>
243
+ <th scope="row">{fact.label}</th>
244
+ <td>
245
+ <Runs paragraph={fact.value} />
246
+ </td>
247
+ </tr>
248
+ ))}
249
+ </tbody>
250
+ </table>
251
+ );
252
+ }
253
+
254
+ function Changes({ changes, labels }: { changes: ReadChange[]; labels: ReadLabels }) {
255
+ return (
256
+ <div className="rd-beat rd-changes">
257
+ <table className="rd-changes-table">
258
+ <thead>
259
+ <tr>
260
+ <th scope="col" className="rd-now">
261
+ {labels.now}
262
+ </th>
263
+ <th scope="col" className="rd-after">
264
+ {labels.after}
265
+ </th>
266
+ </tr>
267
+ </thead>
268
+ <tbody>
269
+ {changes.map((change, i) => (
270
+ // biome-ignore lint/suspicious/noArrayIndexKey: rows are static content in a fixed order
271
+ <tr key={i}>
272
+ <td className="rd-now" data-label={labels.now}>
273
+ {filled(change.now) && <Runs paragraph={change.now} />}
274
+ </td>
275
+ <td className="rd-after" data-label={labels.after}>
276
+ {filled(change.after) && <Runs paragraph={change.after} />}
277
+ </td>
278
+ </tr>
279
+ ))}
280
+ </tbody>
281
+ </table>
282
+ </div>
283
+ );
284
+ }
285
+
286
+ function Recommend({ items, label }: { items: ReadParagraph[]; label: string }) {
287
+ return (
288
+ <div className="rd-beat rd-recommend">
289
+ <h4>{label}</h4>
290
+ <ul className="rd-checks">
291
+ {items.map((item, i) => (
292
+ // biome-ignore lint/suspicious/noArrayIndexKey: recommendations are static content in a fixed order
293
+ <li key={i}>
294
+ <Check size={16} strokeWidth={2.5} aria-hidden="true" />
295
+ <span>
296
+ <Runs paragraph={item} />
297
+ </span>
175
298
  </li>
176
299
  ))}
177
300
  </ul>
178
- </footer>
301
+ </div>
179
302
  );
180
303
  }
181
304
 
@@ -190,14 +313,15 @@ function Gist({
190
313
  gradeKey?: ReadProps["gradeKey"];
191
314
  labels: ReadLabels;
192
315
  }) {
316
+ const key = gradeKey ?? [];
193
317
  return (
194
318
  <section className="rd-gist" aria-label={title ? undefined : labels.gist}>
195
- {(title || gradeKey) && (
319
+ {(title || key.length > 0) && (
196
320
  <div className="op-ov-head">
197
321
  {title && <h2 className="op-ov-title">{title}</h2>}
198
- {gradeKey && (
322
+ {key.length > 0 && (
199
323
  <ul className="op-legend">
200
- {gradeKey.map((entry) => (
324
+ {key.map((entry) => (
201
325
  <li key={entry.grade}>
202
326
  <GradeChip grade={entry.grade} />
203
327
  <span>{entry.label}</span>
@@ -263,9 +387,18 @@ function Gist({
263
387
  }
264
388
 
265
389
  function Section({ section, labels }: { section: ReadSection; labels: ReadLabels }) {
266
- const found = section.found.filter((paragraph) => !isEmpty(paragraph));
267
- const changes = section.changes.filter((paragraph) => !isEmpty(paragraph));
390
+ const tools = (section.tools ?? section.sources ?? []).filter((tool) => tool?.label?.trim());
391
+ const found = (section.found ?? []).filter(filled);
392
+ const facts = (section.facts ?? []).filter((fact) => filled(fact?.value));
393
+ const examples = (section.examples ?? [])
394
+ .map((list) => ({ title: list?.title, items: (list?.items ?? []).filter(filled) }))
395
+ .filter((list) => list.items.length > 0);
268
396
  const shots = section.shots ?? [];
397
+ const changes = (section.changes ?? []).filter(
398
+ (change) => filled(change?.now) || filled(change?.after),
399
+ );
400
+ const recommend = (section.recommend ?? []).filter(filled);
401
+ const showed = found.length + facts.length + examples.length + shots.length > 0;
269
402
  return (
270
403
  <section className="op-topic rd-section" id={section.id}>
271
404
  <header className="op-topic-head">
@@ -282,44 +415,46 @@ function Section({ section, labels }: { section: ReadSection; labels: ReadLabels
282
415
  </>
283
416
  )}
284
417
  </header>
285
- <div className="rd-beat rd-found">
286
- <h4>{labels.found}</h4>
287
- {found.map((paragraph, i) => (
288
- // biome-ignore lint/suspicious/noArrayIndexKey: paragraphs are static content in a fixed order
289
- <Paragraph paragraph={paragraph} key={i} />
290
- ))}
291
- {section.examples && section.examples.length > 0 && (
292
- <ul className="rd-examples">
293
- {section.examples.map((example, i) => (
294
- // biome-ignore lint/suspicious/noArrayIndexKey: examples are static content in a fixed order
295
- <li key={i}>
296
- <Runs paragraph={example} />
297
- </li>
298
- ))}
299
- </ul>
300
- )}
301
- {shots.length > 0 && (
302
- <div className="rd-shots" data-count={Math.min(shots.length, 3)}>
303
- {shots.map((shot) => (
304
- <Shot shot={shot} key={shot.src} />
305
- ))}
306
- </div>
307
- )}
308
- </div>
309
- <div className="rd-beat rd-why">
310
- <h4>{labels.why}</h4>
311
- <Paragraph paragraph={section.why} />
312
- </div>
313
- <div className="rd-beat rd-changes">
314
- <h4>{labels.changes}</h4>
315
- {changes.map((paragraph, i) => (
316
- // biome-ignore lint/suspicious/noArrayIndexKey: paragraphs are static content in a fixed order
317
- <Paragraph paragraph={paragraph} key={i} />
318
- ))}
319
- </div>
320
- {section.sources && section.sources.length > 0 && (
321
- <Sources sources={section.sources} label={labels.sources} />
418
+ {tools.length > 0 && <Tools tools={tools} label={labels.tools} />}
419
+ {showed && (
420
+ <div className="rd-beat rd-found">
421
+ <h4>{labels.found}</h4>
422
+ {found.map((paragraph, i) => (
423
+ // biome-ignore lint/suspicious/noArrayIndexKey: paragraphs are static content in a fixed order
424
+ <Paragraph paragraph={paragraph} key={i} />
425
+ ))}
426
+ {facts.length > 0 && <Facts facts={facts} />}
427
+ {examples.map((list, i) => (
428
+ // biome-ignore lint/suspicious/noArrayIndexKey: lists are static content in a fixed order, and a title may repeat
429
+ <div className="rd-examples-block" key={i}>
430
+ {list.title && <h5>{list.title}</h5>}
431
+ <ul className="rd-examples">
432
+ {list.items.map((example, j) => (
433
+ // biome-ignore lint/suspicious/noArrayIndexKey: examples are static content in a fixed order
434
+ <li key={j}>
435
+ <Runs paragraph={example} />
436
+ </li>
437
+ ))}
438
+ </ul>
439
+ </div>
440
+ ))}
441
+ {shots.length > 0 && (
442
+ <div className="rd-shots" data-count={Math.min(shots.length, 3)}>
443
+ {shots.map((shot) => (
444
+ <Shot shot={shot} key={shot.src} />
445
+ ))}
446
+ </div>
447
+ )}
448
+ </div>
322
449
  )}
450
+ {filled(section.why) && (
451
+ <div className="rd-beat rd-why">
452
+ <h4>{labels.why}</h4>
453
+ <Paragraph paragraph={section.why} />
454
+ </div>
455
+ )}
456
+ {changes.length > 0 && <Changes changes={changes} labels={labels} />}
457
+ {recommend.length > 0 && <Recommend items={recommend} label={labels.recommend} />}
323
458
  </section>
324
459
  );
325
460
  }
@@ -328,9 +463,7 @@ function Plan({ plan, labels }: { plan: ReadPlan; labels: ReadLabels }) {
328
463
  return (
329
464
  <section className="rd-plan" id={plan.id}>
330
465
  <h2>{plan.title ?? labels.plan}</h2>
331
- {plan.lead && !isEmpty(plan.lead) && (
332
- <Paragraph paragraph={plan.lead} className="rd-plan-lead" />
333
- )}
466
+ {filled(plan.lead) && <Paragraph paragraph={plan.lead} className="rd-plan-lead" />}
334
467
  <ol className="rd-steps">
335
468
  {plan.steps.map((step) => (
336
469
  <li className="rd-step" key={step.title}>
@@ -342,7 +475,7 @@ function Plan({ plan, labels }: { plan: ReadPlan; labels: ReadLabels }) {
342
475
  <span className="rd-step-who">{step.who}</span>
343
476
  </>
344
477
  )}
345
- {step.body && !isEmpty(step.body) && (
478
+ {filled(step.body) && (
346
479
  <>
347
480
  {" "}
348
481
  <Runs paragraph={step.body} />
@@ -375,14 +508,14 @@ export function Read({
375
508
  <article className="op-doc rd-doc">
376
509
  <header className="op-head">
377
510
  <p className="op-meta">
378
- Prepared for <b>{preparedFor}</b>
511
+ {labels.preparedFor} <b>{preparedFor}</b>
379
512
  <span className="op-meta-sep" aria-hidden="true">
380
513
  {" · "}
381
514
  </span>
382
515
  <span className="op-meta-date">{date}</span>
383
516
  </p>
384
517
  <h1>{title}</h1>
385
- {!isEmpty(lede) && <Paragraph paragraph={lede} className="op-intro rd-lede" />}
518
+ {filled(lede) && <Paragraph paragraph={lede} className="op-intro rd-lede" />}
386
519
  </header>
387
520
  {shownTiers.length > 0 && (
388
521
  <Gist tiers={shownTiers} title={gistTitle} gradeKey={gradeKey} labels={labels} />
@@ -399,7 +532,7 @@ export function Read({
399
532
  </div>
400
533
  ))}
401
534
  {plan.steps.length > 0 && <Plan plan={plan} labels={labels} />}
402
- {!isEmpty(close.line) && <Paragraph paragraph={close.line} className="rd-closing" />}
535
+ {filled(close.line) && <Paragraph paragraph={close.line} className="rd-closing" />}
403
536
  {close.signOff && close.person && (
404
537
  <footer className="op-close rd-close">
405
538
  <SignOff signOff={close.signOff} person={close.person} />
@@ -2197,11 +2197,12 @@
2197
2197
  THE READ: the baseline's grades explained by cause (Read.tsx). The
2198
2198
  writeup's sibling: it sits inside the writeup's own .op root, so the
2199
2199
  column, the head, the grade chip and its key, the tier heading, the
2200
- section card, the screen, the sources, the close, and every print
2201
- rule of the writeup's are its own already. This block adds only what
2202
- the read has and the writeup does not: the gist table, the three
2203
- beats of a section, the proof pictures in a row, and the numbered
2204
- plan. It sets no @page; the writeup's, earlier in this sheet and the
2200
+ section card, the screen, the close, and every print rule of the
2201
+ writeup's are its own already. This block adds only what the read has
2202
+ and the writeup does not: the gist table, the beats of a section (the
2203
+ tools we looked through, what they showed with its facts and
2204
+ examples, why, today and after, and what we recommend), the proof
2205
+ pictures in a row, and the numbered plan. It sets no @page; the writeup's, earlier in this sheet and the
2205
2206
  later of the two there, sets letter paper for every artifact page.
2206
2207
 
2207
2208
  Tokens only (theme.css).
@@ -2288,7 +2289,7 @@
2288
2289
  min-width: 0;
2289
2290
  }
2290
2291
 
2291
- /* ---- a section: the trade name over the question, then three beats ---- */
2292
+ /* ---- a section: the trade name over the question, then its beats ---- */
2292
2293
  .atelic-ui .rd-name {
2293
2294
  margin: 0 0 4px;
2294
2295
  font-family: var(--font-mono);
@@ -2304,7 +2305,7 @@
2304
2305
  font-size: 21px;
2305
2306
  }
2306
2307
  .atelic-ui .rd-beat {
2307
- margin-top: 16px;
2308
+ margin-top: 20px;
2308
2309
  min-width: 0;
2309
2310
  }
2310
2311
  .atelic-ui .rd-beat h4 {
@@ -2324,12 +2325,99 @@
2324
2325
  .atelic-ui .rd-beat p:last-child {
2325
2326
  margin-bottom: 0;
2326
2327
  }
2328
+ /* Where we looked: the tools in a row at the top of the section, each a
2329
+ link with its line under it. */
2330
+ .atelic-ui .rd-tools-list {
2331
+ list-style: none;
2332
+ margin: 0;
2333
+ padding: 0;
2334
+ display: flex;
2335
+ flex-wrap: wrap;
2336
+ gap: 10px 28px;
2337
+ }
2338
+ .atelic-ui .rd-tools-list li {
2339
+ display: flex;
2340
+ flex-direction: column;
2341
+ gap: 1px;
2342
+ min-width: 0;
2343
+ max-width: 100%;
2344
+ }
2345
+ .atelic-ui .rd-tools-list a,
2346
+ .atelic-ui .rd-tool-name {
2347
+ display: inline-flex;
2348
+ align-items: center;
2349
+ gap: 3px;
2350
+ font-size: 15px;
2351
+ font-weight: 600;
2352
+ color: var(--op-accent);
2353
+ text-decoration: none;
2354
+ }
2355
+ .atelic-ui .rd-tool-name {
2356
+ color: var(--op-ink);
2357
+ }
2358
+ .atelic-ui .rd-tools-list a:hover {
2359
+ text-decoration: underline;
2360
+ text-underline-offset: 2px;
2361
+ }
2362
+ .atelic-ui .rd-tools-list svg {
2363
+ flex: none;
2364
+ }
2365
+ .atelic-ui .rd-tool-note {
2366
+ font-size: 13px;
2367
+ line-height: 1.35;
2368
+ color: var(--op-ink-faint);
2369
+ }
2370
+
2371
+ /* What it showed: the facts as a compact table of label and value, in
2372
+ the writeup's table dress (an ink rule over thin row rules). */
2373
+ .atelic-ui .rd-facts {
2374
+ width: 100%;
2375
+ max-width: 620px;
2376
+ margin: 12px 0 0;
2377
+ border-collapse: collapse;
2378
+ border-top: 1.5px solid var(--op-ink);
2379
+ font-size: 15px;
2380
+ line-height: 1.4;
2381
+ }
2382
+ .atelic-ui .rd-facts th,
2383
+ .atelic-ui .rd-facts td {
2384
+ padding: 7px 14px 7px 0;
2385
+ border-bottom: 1px solid var(--neutral-100);
2386
+ text-align: left;
2387
+ vertical-align: top;
2388
+ }
2389
+ .atelic-ui .rd-facts th {
2390
+ width: 55%;
2391
+ font-weight: 400;
2392
+ color: var(--op-ink-soft);
2393
+ }
2394
+ .atelic-ui .rd-facts td {
2395
+ padding-right: 0;
2396
+ font-weight: 600;
2397
+ color: var(--op-ink);
2398
+ font-variant-numeric: tabular-nums;
2399
+ }
2400
+ .atelic-ui .rd-facts tr:last-child > * {
2401
+ border-bottom: 0;
2402
+ }
2403
+
2404
+ /* The examples: a list or more, each under its own short heading. */
2405
+ .atelic-ui .rd-examples-block {
2406
+ margin-top: 14px;
2407
+ max-width: 68ch;
2408
+ }
2409
+ .atelic-ui .rd-examples-block h5 {
2410
+ margin: 0 0 4px;
2411
+ font-size: 15px;
2412
+ font-weight: 650;
2413
+ color: var(--op-ink);
2414
+ }
2327
2415
  .atelic-ui .rd-examples {
2328
2416
  list-style: disc;
2329
- margin: 8px 0 0;
2417
+ margin: 0;
2330
2418
  padding-left: 1.2em;
2331
- max-width: 68ch;
2332
2419
  color: var(--op-ink-soft);
2420
+ text-wrap: pretty;
2333
2421
  }
2334
2422
  .atelic-ui .rd-examples li + li {
2335
2423
  margin-top: 4px;
@@ -2355,6 +2443,92 @@
2355
2443
  gap: 14px;
2356
2444
  margin-top: 14px;
2357
2445
  }
2446
+ /* A lone picture keeps a screen's size rather than filling the card. */
2447
+ .atelic-ui .rd-shots[data-count="1"] {
2448
+ max-width: 380px;
2449
+ }
2450
+
2451
+ /* Today and after: what a thing is, and what it becomes. Two columns
2452
+ wide; on a phone each row stacks, each half wearing its label. */
2453
+ .atelic-ui .rd-changes-table {
2454
+ width: 100%;
2455
+ border-collapse: collapse;
2456
+ font-size: 15px;
2457
+ line-height: 1.45;
2458
+ }
2459
+ .atelic-ui .rd-changes-table thead {
2460
+ position: absolute;
2461
+ width: 1px;
2462
+ height: 1px;
2463
+ overflow: hidden;
2464
+ clip: rect(0, 0, 0, 0);
2465
+ white-space: nowrap;
2466
+ }
2467
+ .atelic-ui .rd-changes-table,
2468
+ .atelic-ui .rd-changes-table tbody,
2469
+ .atelic-ui .rd-changes-table tr,
2470
+ .atelic-ui .rd-changes-table td {
2471
+ display: block;
2472
+ }
2473
+ .atelic-ui .rd-changes-table tbody tr {
2474
+ padding: 10px 0;
2475
+ border-bottom: 1px solid var(--neutral-100);
2476
+ }
2477
+ .atelic-ui .rd-changes-table tbody tr:first-child {
2478
+ border-top: 1.5px solid var(--op-ink);
2479
+ }
2480
+ .atelic-ui .rd-changes-table tbody tr:last-child {
2481
+ border-bottom: 0;
2482
+ padding-bottom: 0;
2483
+ }
2484
+ .atelic-ui .rd-changes-table td {
2485
+ text-align: left;
2486
+ text-wrap: pretty;
2487
+ }
2488
+ .atelic-ui .rd-changes-table td + td {
2489
+ margin-top: 6px;
2490
+ }
2491
+ .atelic-ui .rd-changes-table td::before {
2492
+ content: attr(data-label);
2493
+ display: block;
2494
+ margin-bottom: 1px;
2495
+ font-family: var(--font-mono);
2496
+ font-size: 11px;
2497
+ letter-spacing: 0.1em;
2498
+ text-transform: uppercase;
2499
+ color: var(--op-ink-faint);
2500
+ }
2501
+ .atelic-ui .rd-changes-table td.rd-now {
2502
+ color: var(--op-ink-soft);
2503
+ }
2504
+ .atelic-ui .rd-changes-table td.rd-after {
2505
+ color: var(--op-ink);
2506
+ }
2507
+ .atelic-ui .rd-changes-table td.rd-after::before {
2508
+ color: var(--op-accent);
2509
+ }
2510
+
2511
+ /* What we recommend: a checklist, each item led by the accent's check. */
2512
+ .atelic-ui .rd-checks {
2513
+ list-style: none;
2514
+ margin: 0;
2515
+ padding: 0;
2516
+ display: grid;
2517
+ gap: 8px;
2518
+ max-width: 68ch;
2519
+ }
2520
+ .atelic-ui .rd-checks li {
2521
+ display: grid;
2522
+ grid-template-columns: auto minmax(0, 1fr);
2523
+ column-gap: 10px;
2524
+ align-items: start;
2525
+ line-height: 1.45;
2526
+ text-wrap: pretty;
2527
+ }
2528
+ .atelic-ui .rd-checks svg {
2529
+ margin-top: 0.2em;
2530
+ color: var(--op-accent);
2531
+ }
2358
2532
 
2359
2533
  /* ---- the plan: the steps in order, numbered ---- */
2360
2534
  .atelic-ui .rd-plan {
@@ -2448,6 +2622,69 @@
2448
2622
  padding-left: 20px;
2449
2623
  padding-right: 16px;
2450
2624
  }
2625
+ .atelic-ui .rd-facts th {
2626
+ width: 45%;
2627
+ }
2628
+ .atelic-ui .rd-changes-table {
2629
+ display: table;
2630
+ table-layout: fixed;
2631
+ }
2632
+ .atelic-ui .rd-changes-table thead {
2633
+ display: table-header-group;
2634
+ position: static;
2635
+ width: auto;
2636
+ height: auto;
2637
+ overflow: visible;
2638
+ clip: auto;
2639
+ }
2640
+ .atelic-ui .rd-changes-table tbody {
2641
+ display: table-row-group;
2642
+ }
2643
+ .atelic-ui .rd-changes-table tr,
2644
+ .atelic-ui .rd-changes-table tbody tr {
2645
+ display: table-row;
2646
+ padding: 0;
2647
+ border: 0;
2648
+ }
2649
+ .atelic-ui .rd-changes-table th,
2650
+ .atelic-ui .rd-changes-table td {
2651
+ display: table-cell;
2652
+ width: 50%;
2653
+ padding: 9px 18px 9px 0;
2654
+ border-bottom: 1px solid var(--neutral-100);
2655
+ text-align: left;
2656
+ vertical-align: top;
2657
+ }
2658
+ .atelic-ui .rd-changes-table th + th,
2659
+ .atelic-ui .rd-changes-table td + td {
2660
+ margin-top: 0;
2661
+ padding-left: 18px;
2662
+ padding-right: 0;
2663
+ border-left: 1px solid var(--neutral-100);
2664
+ }
2665
+ .atelic-ui .rd-changes-table td::before {
2666
+ content: none;
2667
+ }
2668
+ .atelic-ui .rd-changes-table tbody tr:first-child {
2669
+ border-top: 0;
2670
+ }
2671
+ .atelic-ui .rd-changes-table thead th {
2672
+ padding-top: 0;
2673
+ padding-bottom: 8px;
2674
+ border-bottom: 1.5px solid var(--op-ink);
2675
+ font-family: var(--font-mono);
2676
+ font-size: 11.5px;
2677
+ font-weight: 500;
2678
+ letter-spacing: 0.1em;
2679
+ text-transform: uppercase;
2680
+ color: var(--op-ink-faint);
2681
+ }
2682
+ .atelic-ui .rd-changes-table thead th.rd-after {
2683
+ color: var(--op-accent);
2684
+ }
2685
+ .atelic-ui .rd-changes-table tbody tr:last-child td {
2686
+ border-bottom: 0;
2687
+ }
2451
2688
  .atelic-ui .rd-shots[data-count="2"] {
2452
2689
  grid-template-columns: repeat(2, minmax(0, 1fr));
2453
2690
  }
@@ -2465,6 +2702,10 @@
2465
2702
  border-color: var(--neutral-200);
2466
2703
  }
2467
2704
  .atelic-ui .rd-gist-table tr,
2705
+ .atelic-ui .rd-changes-table tr,
2706
+ .atelic-ui .rd-facts,
2707
+ .atelic-ui .rd-tools,
2708
+ .atelic-ui .rd-checks li,
2468
2709
  .atelic-ui .rd-closing,
2469
2710
  .atelic-ui .rd-why,
2470
2711
  .atelic-ui .rd-step,
@@ -2473,6 +2714,7 @@
2473
2714
  break-inside: avoid;
2474
2715
  }
2475
2716
  .atelic-ui .rd-beat h4,
2717
+ .atelic-ui .rd-examples-block h5,
2476
2718
  .atelic-ui .rd-plan h2 {
2477
2719
  break-after: avoid;
2478
2720
  }