@atelic-action/artifact 2.0.0 → 4.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 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 `/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.
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 between the gist and the cards; 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
 
@@ -26,8 +26,8 @@ Every export of the package, all from the one entry, `@atelic-action/artifact`.
26
26
  | `ratingCell` | A star rating table cell for a writeup table, sorted by its figure | `/writeup` |
27
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 |
28
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 |
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` |
29
+ | `Read`, `ReadProps` | The baseline's grades explained by cause: the gist table, each row a link to its card, with an optional `note` callout over 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). The plan, numbered and in order, sits between the gist and the cards, so the whole program reads before any one card's detail. A beat with no data is left out | `/the-read` |
30
+ | `readLabels`, `ReadLabels` | The fixed words the read sets (Prepared for, the scorecard's screen reader words, Today, After, Recommendations, the plan's heading), each overridable through `labels` | `/the-read` |
31
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 |
32
32
  | `Report`, `ReportProps` | The monthly page: the four questions, the seven grades now and before, the sign off, on one letter sheet | `/report` |
33
33
  | `ReportLead`, `ReportLeads`, `ReportGradeRow` | The shapes a monthly page's content is written in | none |
@@ -45,7 +45,7 @@ What a document is built from stays in `@atelic-action/ui`, and this package imp
45
45
  bun add @atelic-action/artifact @atelic-action/ui
46
46
  ```
47
47
 
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
+ Peer dependencies: `@atelic-action/ui` at `^3.0.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.
49
49
 
50
50
  ```tsx
51
51
  import { Writeup } from "@atelic-action/artifact";
@@ -136,6 +136,8 @@ Static files. `vercel.json` points Vercel at `dist/client` with no framework pre
136
136
 
137
137
  A document is fixed or grown here, in `src/documents/`, with its test in `tst/documents/`, and released as a version of this package; a folder takes it with `bun update @atelic-action/artifact`. A piece a document is built from (the grade chip, the sign off, a page component, the chrome, the wall, a command) is fixed in `@atelic-action/ui` and released there first, then taken here by bumping the ui range. An improvement to the seed itself (a route, a stub file, the example content) is made here and copied into the folders by hand.
138
138
 
139
+ A folder taking 4.0.0 takes ui 3.0.0 with it (`bun update @atelic-action/artifact @atelic-action/ui --latest --cwd <short>`) and repoints its own routes, since ui 3.0.0 keeps acronyms capitalized with no alias for the old spelling: `canonicalUrl` from `@atelic-action/ui/artifact` becomes `canonicalURL` in `src/routes/index.tsx`, and the root route's `baseCss` import becomes `baseCSS`.
140
+
139
141
  ## Releasing
140
142
 
141
143
  1. Bump `version` in `package.json` and merge it to `main`: patch for a fix, minor for a new document or prop, major for a breaking prop, type, or class rename.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atelic-action/artifact",
3
- "version": "2.0.0",
3
+ "version": "4.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.1.0",
40
+ "@atelic-action/ui": "^3.0.0",
41
41
  "lucide-react": "^1.23.0",
42
42
  "react": "^19.0.0"
43
43
  },
44
44
  "devDependencies": {
45
- "@atelic-action/ui": "^2.1.0",
45
+ "@atelic-action/ui": "^3.0.0",
46
46
  "@biomejs/biome": "^2.0.0",
47
47
  "@playwright/test": "^1.60.0",
48
48
  "@sentry/react": "^10.0.0",
@@ -227,7 +227,7 @@ export function Gate({
227
227
  errorText = "That email does not open this document. Use the address it was sent to.",
228
228
  onUnlock,
229
229
  }: GateProps) {
230
- const [html, setHtml] = useState<string | null>(null);
230
+ const [html, setHTML] = useState<string | null>(null);
231
231
  const [failed, setFailed] = useState(false);
232
232
  const [busy, setBusy] = useState(false);
233
233
  const [value, setValue] = useState("");
@@ -253,7 +253,7 @@ export function Gate({
253
253
  if (opened) {
254
254
  writeStored(storageKey, fromQuery);
255
255
  identifyReader(opened.email, identifyDomain);
256
- setHtml(opened.html);
256
+ setHTML(opened.html);
257
257
  onUnlockRef.current?.(opened.person);
258
258
  return;
259
259
  }
@@ -267,7 +267,7 @@ export function Gate({
267
267
  const opened = await openDocument(payload, stored);
268
268
  if (cancelled || !opened) return;
269
269
  identifyReader(opened.email, identifyDomain);
270
- setHtml(opened.html);
270
+ setHTML(opened.html);
271
271
  onUnlockRef.current?.(opened.person);
272
272
  }
273
273
 
@@ -290,7 +290,7 @@ export function Gate({
290
290
  }
291
291
  writeStored(storageKey, value);
292
292
  identifyReader(opened.email, identifyDomain);
293
- setHtml(opened.html);
293
+ setHTML(opened.html);
294
294
  onUnlockRef.current?.(opened.person);
295
295
  }
296
296
 
@@ -1,17 +1,16 @@
1
1
  import {
2
2
  type ArtifactLink,
3
3
  type ArtifactParagraph,
4
- GradeChip,
5
- gradeLabel,
6
4
  isEmpty,
5
+ newTabProps,
7
6
  Paragraph,
8
7
  Runs,
9
8
  SignOff,
10
9
  type WriteupGrade,
11
10
  type WriteupPerson,
12
11
  } from "@atelic-action/ui/artifact";
13
- import { IconList, type Tone } from "@atelic-action/ui/components";
14
- import { Check } from "lucide-react";
12
+ import { IconList, ReportCard, type Tone } from "@atelic-action/ui/components";
13
+ import { ArrowUpRight, Check } from "lucide-react";
15
14
  import type { ReactNode } from "react";
16
15
  import { Topic, type WriteupTopic } from "./Writeup";
17
16
 
@@ -87,6 +86,8 @@ export interface ReadTier {
87
86
  id?: string;
88
87
  /** The tier's name, e.g. "Health". */
89
88
  title: string;
89
+ /** Where the name is defined, e.g. the practice's glossary; the heading links there when set. */
90
+ href?: string;
90
91
  /** The questions the tier answers, in the owner's words. */
91
92
  lead?: string;
92
93
  sections: ReadSection[];
@@ -98,9 +99,11 @@ export interface ReadStep {
98
99
  body?: ReadParagraph;
99
100
  /** Whose step it is when it is not the practice's, e.g. "Yours" or a name; set beside the title. */
100
101
  who?: string;
102
+ /** The cards the step draws on, linked in a line beneath it, e.g. { text: "Local SEO", href: "#local-seo" }. */
103
+ cards?: ReadLink[];
101
104
  }
102
105
 
103
- /** The plan: the steps in order, numbered. */
106
+ /** The plan: the steps in order, numbered, between the gist and the cards, so the whole program reads before any one card's detail. */
104
107
  export interface ReadPlan {
105
108
  /** The anchor a header stop points at, e.g. "plan". */
106
109
  id?: string;
@@ -123,12 +126,10 @@ export interface ReadClose {
123
126
  export interface ReadLabels {
124
127
  /** The dateline's words before the business, e.g. "Prepared for". */
125
128
  preparedFor: string;
126
- /** The gist table's first column. */
127
- section: string;
128
- /** The gist table's second column. */
129
- grade: string;
130
- /** The gist table's third column. */
129
+ /** What a screen reader calls the gist card when it has no title. */
131
130
  gist: string;
131
+ /** What a screen reader hears before a letter, on the gist card and each section's card. */
132
+ grade: string;
132
133
  /** What a screen reader hears for a section with no grade. */
133
134
  notGraded: string;
134
135
  /** The today and after table's first column. */
@@ -147,9 +148,8 @@ export interface ReadLabels {
147
148
 
148
149
  export const readLabels: ReadLabels = {
149
150
  preparedFor: "Prepared for",
150
- section: "Section",
151
- grade: "Grade",
152
151
  gist: "The Gist",
152
+ grade: "Grade",
153
153
  notGraded: "Not graded yet",
154
154
  now: "Today",
155
155
  after: "After",
@@ -167,9 +167,13 @@ export interface ReadProps {
167
167
  date: string;
168
168
  /** Two or three sentences under the title. */
169
169
  lede: ReadParagraph;
170
- /** The heading over the gist table, e.g. "At a Glance". */
170
+ /** The heading over the scorecard, e.g. "The Scorecard". */
171
171
  gistTitle?: string;
172
- /** A callout under the gist table, e.g. what the read leaves out and when it follows. */
172
+ /** The line under the scorecard's heading, e.g. what it is. */
173
+ gistLead?: string;
174
+ /** The anchor a header stop points at for the scorecard, e.g. "scorecard". */
175
+ gistId?: string;
176
+ /** A callout over the scorecard, under its heading, e.g. what the read leaves out and when it follows. */
173
177
  note?: ReadParagraph;
174
178
  /** The key: what each letter means, in a few words each. */
175
179
  gradeKey?: Array<{ grade: WriteupGrade; label: string }>;
@@ -261,85 +265,51 @@ function Recommend({ items, label }: { items: ReadRecommendation[]; label: strin
261
265
 
262
266
  function Gist({
263
267
  tiers,
268
+ id,
264
269
  title,
270
+ lead,
271
+ note,
265
272
  gradeKey,
266
273
  labels,
267
274
  }: {
268
275
  tiers: ReadTier[];
276
+ id?: string;
269
277
  title?: string;
278
+ lead?: string;
279
+ note?: ReadProps["note"];
270
280
  gradeKey?: ReadProps["gradeKey"];
271
281
  labels: ReadLabels;
272
282
  }) {
273
- const key = gradeKey ?? [];
274
283
  return (
275
- <section className="rd-gist" aria-label={title ? undefined : labels.gist}>
276
- {(title || key.length > 0) && (
277
- <div className="op-ov-head">
278
- {title && <h2 className="op-ov-title">{title}</h2>}
279
- {key.length > 0 && (
280
- <ul className="op-legend">
281
- {key.map((entry) => (
282
- <li key={entry.grade}>
283
- <GradeChip grade={entry.grade} />
284
- <span>{entry.label}</span>
285
- </li>
286
- ))}
287
- </ul>
288
- )}
284
+ <>
285
+ {title && (
286
+ <div className="op-part" id={id}>
287
+ <h2>{title}</h2>
288
+ {lead && <p>{lead}</p>}
289
289
  </div>
290
290
  )}
291
- <div className="rd-gist-card">
292
- <table className="rd-gist-table">
293
- <thead>
294
- <tr>
295
- <th scope="col" className="rd-gist-section">
296
- {labels.section}
297
- </th>
298
- <th scope="col" className="rd-gist-grade">
299
- {labels.grade}
300
- </th>
301
- <th scope="col" className="rd-gist-read">
302
- {labels.gist}
303
- </th>
304
- </tr>
305
- </thead>
306
- {tiers.map((tier) => (
307
- <tbody key={tier.title}>
308
- <tr className="rd-gist-tier">
309
- <th scope="rowgroup" colSpan={3}>
310
- {tier.title}
311
- </th>
312
- </tr>
313
- {tier.sections.map((section) => (
314
- <tr key={section.id}>
315
- <th scope="row" className="rd-gist-section">
316
- <a href={`#${section.id}`}>{section.title}</a>
317
- </th>
318
- <td className="rd-gist-grade">
319
- {section.grade ? (
320
- <>
321
- <span className="op-sr-only">{gradeLabel[section.grade]}</span>
322
- <span aria-hidden="true">
323
- <GradeChip grade={section.grade} />
324
- </span>
325
- </>
326
- ) : (
327
- <>
328
- <span className="op-sr-only">{labels.notGraded}</span>
329
- <span className="op-dash" aria-hidden="true" />
330
- </>
331
- )}
332
- </td>
333
- <td className="rd-gist-read">
334
- <Runs paragraph={section.gist} />
335
- </td>
336
- </tr>
337
- ))}
338
- </tbody>
339
- ))}
340
- </table>
341
- </div>
342
- </section>
291
+ {filled(note) && (
292
+ <div className="rd-note" role="note">
293
+ <Paragraph paragraph={note} />
294
+ </div>
295
+ )}
296
+ <ReportCard
297
+ className="rd-gist"
298
+ label={title ?? labels.gist}
299
+ legend={gradeKey}
300
+ legendAt="foot"
301
+ labels={{ grade: labels.grade, notGraded: labels.notGraded }}
302
+ groups={tiers.map((tier) => ({
303
+ title: tier.title,
304
+ rows: tier.sections.map((section) => ({
305
+ name: section.title,
306
+ href: `#${section.id}`,
307
+ grade: section.grade,
308
+ line: <Runs paragraph={section.gist} />,
309
+ })),
310
+ }))}
311
+ />
312
+ </>
343
313
  );
344
314
  }
345
315
 
@@ -392,21 +362,24 @@ function Plan({ plan, labels }: { plan: ReadPlan; labels: ReadLabels }) {
392
362
  <ol className="rd-steps">
393
363
  {plan.steps.map((step) => (
394
364
  <li className="rd-step" key={step.title}>
395
- <p>
396
- <b className="rd-step-title">{step.title}</b>
397
- {step.who && (
398
- <>
399
- {" "}
400
- <span className="rd-step-who">{step.who}</span>
401
- </>
365
+ <div className="rd-step-body">
366
+ <p className="rd-step-head">
367
+ <b className="rd-step-title">{step.title}</b>
368
+ {step.who && <span className="rd-step-who">{step.who}</span>}
369
+ </p>
370
+ {filled(step.body) && <Paragraph paragraph={step.body} />}
371
+ {(step.cards ?? []).some((card) => card?.href) && (
372
+ <p className="rd-step-cards">
373
+ {(step.cards ?? [])
374
+ .filter((card) => card?.href)
375
+ .map((card, i) => (
376
+ <a href={card.href} key={`${i}-${card.href}`}>
377
+ {card.text}
378
+ </a>
379
+ ))}
380
+ </p>
402
381
  )}
403
- {filled(step.body) && (
404
- <>
405
- {" "}
406
- <Runs paragraph={step.body} />
407
- </>
408
- )}
409
- </p>
382
+ </div>
410
383
  </li>
411
384
  ))}
412
385
  </ol>
@@ -420,6 +393,8 @@ export function Read({
420
393
  date,
421
394
  lede,
422
395
  gistTitle,
396
+ gistLead,
397
+ gistId,
423
398
  note,
424
399
  gradeKey,
425
400
  tiers,
@@ -444,19 +419,30 @@ export function Read({
444
419
  {filled(lede) && <Paragraph paragraph={lede} className="op-intro rd-lede" />}
445
420
  </header>
446
421
  {shownTiers.length > 0 && (
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
- </>
422
+ <Gist
423
+ tiers={shownTiers}
424
+ id={gistId}
425
+ title={gistTitle}
426
+ lead={gistLead}
427
+ note={note}
428
+ gradeKey={gradeKey}
429
+ labels={labels}
430
+ />
455
431
  )}
432
+ {plan.steps.length > 0 && <Plan plan={plan} labels={labels} />}
456
433
  {shownTiers.map((tier) => (
457
434
  <div className="rd-tier" key={tier.title}>
458
435
  <div className="op-part" id={tier.id}>
459
- <h2>{tier.title}</h2>
436
+ <h2>
437
+ {tier.href ? (
438
+ <a href={tier.href} {...newTabProps(tier.href, true)}>
439
+ {tier.title}
440
+ <ArrowUpRight size={14} aria-hidden="true" />
441
+ </a>
442
+ ) : (
443
+ tier.title
444
+ )}
445
+ </h2>
460
446
  {tier.lead && <p>{tier.lead}</p>}
461
447
  </div>
462
448
  {tier.sections.map((section) => (
@@ -464,7 +450,6 @@ export function Read({
464
450
  ))}
465
451
  </div>
466
452
  ))}
467
- {plan.steps.length > 0 && <Plan plan={plan} labels={labels} />}
468
453
  {filled(close.line) && <Paragraph paragraph={close.line} className="rd-closing" />}
469
454
  {close.signOff && close.person && (
470
455
  <footer className="op-close rd-close">
@@ -18,6 +18,7 @@ import {
18
18
  type ColumnChartGroup,
19
19
  DataTable,
20
20
  Disclosure,
21
+ ReportCard,
21
22
  StackedBar,
22
23
  type StackedBarSegment,
23
24
  type StatItem,
@@ -27,7 +28,7 @@ import {
27
28
  type ThresholdZone,
28
29
  type Tone,
29
30
  } from "@atelic-action/ui/components";
30
- import { ArrowUpRight, ChevronRight, CircleAlert, CircleCheck, CircleX, Star } from "lucide-react";
31
+ import { ArrowUpRight, CircleAlert, CircleCheck, CircleX, Star } from "lucide-react";
31
32
  import type { ReactNode } from "react";
32
33
 
33
34
  export type { WriteupGrade, WriteupPerson, WriteupShot } from "@atelic-action/ui/artifact";
@@ -609,61 +610,30 @@ export function Topic({ topic, extra }: { topic: WriteupTopic; extra?: ReactNode
609
610
  }
610
611
 
611
612
  function Summary({ summary }: { summary: WriteupSummary }) {
612
- const tiers: Array<{ tier: string; rows: WriteupSummaryRow[] }> = [];
613
+ const groups: Array<{ title: string; rows: WriteupSummaryRow[] }> = [];
613
614
  for (const row of summary.rows) {
614
- const last = tiers[tiers.length - 1];
615
- if (last && last.tier === row.tier) last.rows.push(row);
616
- else tiers.push({ tier: row.tier, rows: [row] });
615
+ const last = groups[groups.length - 1];
616
+ if (last && last.title === row.tier) last.rows.push(row);
617
+ else groups.push({ title: row.tier, rows: [row] });
617
618
  }
618
- const beforeLabel = summary.beforeLabel ?? "Before";
619
619
  return (
620
- <section className="op-ov" id={summary.id}>
621
- <div className="op-ov-head">
622
- {summary.title && <h2 className="op-ov-title">{summary.title}</h2>}
623
- {summary.legend && (
624
- <ul className="op-legend">
625
- {summary.legend.map((entry) => (
626
- <li key={entry.grade}>
627
- <GradeChip grade={entry.grade} />
628
- <span>{entry.label}</span>
629
- </li>
630
- ))}
631
- </ul>
632
- )}
633
- </div>
634
- <div className="op-ov-card">
635
- {tiers.map(({ tier, rows }) => (
636
- <div className="op-ov-tier" key={tier}>
637
- <div className="op-ov-tier-label">{tier}</div>
638
- <ul>
639
- {rows.map((row) => (
640
- <li className="op-ov-row" key={row.section}>
641
- <span className="op-ov-grade">
642
- {row.before && (
643
- <span className="op-ov-before">
644
- <span className="op-sr-only">{`${beforeLabel}: ${gradeLabel[row.before]}. `}</span>
645
- <span aria-hidden="true">
646
- <GradeChip grade={row.before} />
647
- </span>
648
- </span>
649
- )}
650
- <span className="op-sr-only">{`Grade ${gradeLabel[row.grade]}: `}</span>
651
- <span aria-hidden="true">
652
- <GradeChip grade={row.grade} />
653
- </span>
654
- </span>
655
- <b className="op-ov-name">
656
- {row.href ? <a href={row.href}>{row.section}</a> : row.section}
657
- </b>
658
- <span className="op-ov-read">{row.read}</span>
659
- {row.href && <ChevronRight className="op-ov-go" size={18} aria-hidden="true" />}
660
- </li>
661
- ))}
662
- </ul>
663
- </div>
664
- ))}
665
- </div>
666
- </section>
620
+ <ReportCard
621
+ className="op-ov"
622
+ id={summary.id}
623
+ title={summary.title}
624
+ legend={summary.legend}
625
+ labels={{ before: summary.beforeLabel }}
626
+ groups={groups.map((group) => ({
627
+ title: group.title,
628
+ rows: group.rows.map((row) => ({
629
+ name: row.section,
630
+ href: row.href,
631
+ grade: row.grade,
632
+ before: row.before,
633
+ line: row.read,
634
+ })),
635
+ }))}
636
+ />
667
637
  );
668
638
  }
669
639
 
@@ -921,134 +921,11 @@
921
921
 
922
922
  /* ---- the overview: the key, then every section under its tier, each a
923
923
  grade, a name that links to the section, and one plain read ---- */
924
+ /* the overview is the ReportCard (@atelic-action/ui); this is its place on the page */
924
925
  .atelic-ui .op-ov {
925
926
  margin-top: 22px;
926
927
  scroll-margin-top: calc(var(--nav-height) + 16px);
927
928
  }
928
- .atelic-ui .op-ov-head {
929
- display: flex;
930
- flex-wrap: wrap;
931
- align-items: center;
932
- justify-content: space-between;
933
- gap: 8px 20px;
934
- margin-bottom: 10px;
935
- }
936
- .atelic-ui .op-ov-title {
937
- font-size: 13px;
938
- font-family: var(--font-mono);
939
- font-weight: 500;
940
- letter-spacing: 0.1em;
941
- text-transform: uppercase;
942
- color: var(--op-ink-faint);
943
- margin: 0;
944
- }
945
- .atelic-ui .op-legend {
946
- list-style: none;
947
- margin: 0;
948
- padding: 0;
949
- display: flex;
950
- flex-wrap: wrap;
951
- gap: 6px 16px;
952
- font-size: 13.5px;
953
- color: var(--op-ink-soft);
954
- }
955
- .atelic-ui .op-legend li {
956
- display: inline-flex;
957
- align-items: center;
958
- gap: 7px;
959
- }
960
- .atelic-ui .op-legend .op-grade {
961
- width: 22px;
962
- height: 22px;
963
- border-radius: 6px;
964
- font-size: 12px;
965
- }
966
- .atelic-ui .op-ov-card {
967
- background: var(--op-card);
968
- border: 1px solid var(--op-rule);
969
- border-radius: var(--radius-md);
970
- box-shadow: var(--shadow-sm);
971
- overflow: hidden;
972
- }
973
- .atelic-ui .op-ov-tier-label {
974
- padding: 7px 16px;
975
- background: var(--surface-alt);
976
- font-family: var(--font-mono);
977
- font-size: 11.5px;
978
- letter-spacing: 0.1em;
979
- text-transform: uppercase;
980
- color: var(--op-ink-soft);
981
- }
982
- .atelic-ui .op-ov-tier ul {
983
- list-style: none;
984
- margin: 0;
985
- padding: 0;
986
- }
987
- .atelic-ui .op-ov-row {
988
- position: relative;
989
- display: grid;
990
- grid-template-columns: auto minmax(0, 1fr) auto;
991
- column-gap: 12px;
992
- row-gap: 2px;
993
- align-items: start;
994
- padding: 12px 12px 12px 16px;
995
- border-top: 1px solid var(--op-rule);
996
- }
997
- .atelic-ui .op-ov-row:hover {
998
- background: var(--surface);
999
- }
1000
- .atelic-ui .op-ov-grade {
1001
- grid-row: 1 / span 2;
1002
- display: inline-flex;
1003
- align-items: center;
1004
- gap: 6px;
1005
- }
1006
- .atelic-ui .op-ov-grade .op-grade {
1007
- width: 30px;
1008
- height: 30px;
1009
- border-radius: 9px;
1010
- font-size: 15px;
1011
- }
1012
- .atelic-ui .op-ov-before .op-grade {
1013
- opacity: 0.45;
1014
- }
1015
- .atelic-ui .op-ov-name {
1016
- font-size: 16px;
1017
- font-weight: 650;
1018
- letter-spacing: -0.01em;
1019
- line-height: 1.35;
1020
- padding-top: 3px;
1021
- }
1022
- /* The name is the link, and it covers the row; a link inside the read
1023
- sits above it. */
1024
- .atelic-ui .op-ov-name a::after {
1025
- content: "";
1026
- position: absolute;
1027
- inset: 0;
1028
- }
1029
- .atelic-ui .op-ov-row:hover .op-ov-name {
1030
- color: var(--op-accent);
1031
- }
1032
- .atelic-ui .op-ov-read {
1033
- grid-column: 2;
1034
- font-size: 15px;
1035
- line-height: 1.45;
1036
- color: var(--op-ink-soft);
1037
- text-wrap: pretty;
1038
- }
1039
- .atelic-ui .op-ov-read a {
1040
- position: relative;
1041
- z-index: 1;
1042
- color: var(--op-accent);
1043
- text-decoration: underline;
1044
- text-underline-offset: 2px;
1045
- }
1046
- .atelic-ui .op-ov-go {
1047
- grid-column: 3;
1048
- grid-row: 1;
1049
- margin-top: 5px;
1050
- color: var(--neutral-300);
1051
- }
1052
929
 
1053
930
  /* ---- a part: the tier and the question it answers ---- */
1054
931
  .atelic-ui .op-part {
@@ -1068,6 +945,23 @@
1068
945
  font-size: 16.5px;
1069
946
  color: var(--op-ink-soft);
1070
947
  }
948
+ /* a part's name linked to its definition, the way a card's name is */
949
+ .atelic-ui .op-part h2 a {
950
+ display: inline-flex;
951
+ align-items: center;
952
+ gap: 6px;
953
+ color: inherit;
954
+ text-decoration: none;
955
+ border-bottom: 1px solid var(--neutral-200);
956
+ }
957
+ .atelic-ui .op-part h2 a svg {
958
+ color: var(--op-ink-faint);
959
+ }
960
+ .atelic-ui .op-part h2 a:hover,
961
+ .atelic-ui .op-part h2 a:hover svg {
962
+ color: var(--op-accent);
963
+ border-color: var(--op-accent);
964
+ }
1071
965
 
1072
966
  /* ---- a topic: one section, a card of blocks ---- */
1073
967
  .atelic-ui .op-topic {
@@ -1549,24 +1443,6 @@
1549
1443
  .atelic-ui .op-intro {
1550
1444
  font-size: 18px;
1551
1445
  }
1552
- .atelic-ui .op-ov-tier-label {
1553
- padding: 7px 20px;
1554
- }
1555
- .atelic-ui .op-ov-row {
1556
- grid-template-columns: auto 232px minmax(0, 1fr) auto;
1557
- column-gap: 16px;
1558
- padding: 11px 14px 11px 20px;
1559
- }
1560
- .atelic-ui .op-ov-grade {
1561
- grid-row: 1;
1562
- }
1563
- .atelic-ui .op-ov-read {
1564
- grid-column: 3;
1565
- padding-top: 4px;
1566
- }
1567
- .atelic-ui .op-ov-go {
1568
- grid-column: 4;
1569
- }
1570
1446
  .atelic-ui .op-part {
1571
1447
  margin: 40px 0 12px;
1572
1448
  }
@@ -1682,8 +1558,8 @@
1682
1558
  html:has(.atelic-ui .op-doc) {
1683
1559
  zoom: 0.74;
1684
1560
  }
1685
- .atelic-ui .op-ov-go,
1686
- .atelic-ui .op-topic h3 a svg {
1561
+ .atelic-ui .op-topic h3 a svg,
1562
+ .atelic-ui .op-part h2 a svg {
1687
1563
  display: none;
1688
1564
  }
1689
1565
  .atelic-ui,
@@ -1728,12 +1604,12 @@
1728
1604
  width: auto;
1729
1605
  display: block;
1730
1606
  }
1731
- .atelic-ui .op-ov-card,
1732
1607
  .atelic-ui .op-topic {
1733
1608
  box-shadow: none;
1734
1609
  border-color: var(--neutral-200);
1735
1610
  }
1736
- .atelic-ui .op-topic h3 a {
1611
+ .atelic-ui .op-topic h3 a,
1612
+ .atelic-ui .op-part h2 a {
1737
1613
  border-bottom: 0;
1738
1614
  }
1739
1615
  /* A small block stays whole; a table or a disclosure may run over a
@@ -2177,7 +2053,6 @@
2177
2053
  .atelic-ui .rd-note a,
2178
2054
  .atelic-ui .rd-doc .op-why a,
2179
2055
  .atelic-ui .rd-beat a,
2180
- .atelic-ui .rd-gist-read a,
2181
2056
  .atelic-ui .rd-plan a,
2182
2057
  .atelic-ui .rd-closing a {
2183
2058
  color: var(--op-accent);
@@ -2187,72 +2062,12 @@
2187
2062
  overflow-wrap: anywhere;
2188
2063
  }
2189
2064
 
2190
- /* ---- the gist table: section, grade, and one line, under its tier ---- */
2065
+ /* ---- the gist: the ReportCard (@atelic-action/ui), and this is its place on the page ---- */
2191
2066
  .atelic-ui .rd-gist {
2192
2067
  margin-top: 26px;
2193
2068
  }
2194
- .atelic-ui .rd-gist-card {
2195
- min-width: 0;
2196
- background: var(--op-card);
2197
- border: 1px solid var(--op-rule);
2198
- border-radius: var(--radius-md);
2199
- box-shadow: var(--shadow-sm);
2200
- overflow: hidden;
2201
- }
2202
- .atelic-ui .rd-gist-table {
2203
- width: 100%;
2204
- border-collapse: collapse;
2205
- font-size: 15px;
2206
- line-height: 1.45;
2207
- }
2208
- .atelic-ui .rd-gist-table thead th {
2209
- padding: 9px 12px;
2210
- text-align: left;
2211
- font-family: var(--font-mono);
2212
- font-size: 11px;
2213
- font-weight: 500;
2214
- letter-spacing: 0.1em;
2215
- text-transform: uppercase;
2216
- color: var(--op-ink-faint);
2217
- border-bottom: 1px solid var(--op-rule);
2218
- }
2219
- .atelic-ui .rd-gist-tier th {
2220
- padding: 7px 12px;
2221
- background: var(--surface-alt);
2222
- text-align: left;
2223
- font-family: var(--font-mono);
2224
- font-size: 11.5px;
2225
- font-weight: 500;
2226
- letter-spacing: 0.1em;
2227
- text-transform: uppercase;
2228
- color: var(--op-ink-soft);
2229
- }
2230
- .atelic-ui .rd-gist-table tbody tr + tr > * {
2231
- border-top: 1px solid var(--op-rule);
2232
- }
2233
- .atelic-ui .rd-gist-table tbody th[scope="row"],
2234
- .atelic-ui .rd-gist-table td {
2235
- padding: 11px 12px;
2236
- vertical-align: top;
2237
- text-align: left;
2238
- }
2239
- .atelic-ui .rd-gist-table .rd-gist-section {
2240
- font-weight: 650;
2241
- letter-spacing: -0.01em;
2242
- width: 30%;
2243
- }
2244
- .atelic-ui .rd-gist-table tbody .rd-gist-section a:hover {
2245
- color: var(--op-accent);
2246
- }
2247
- .atelic-ui .rd-gist-table .rd-gist-grade {
2248
- width: 1%;
2249
- white-space: nowrap;
2250
- text-align: center;
2251
- }
2252
- .atelic-ui .rd-gist-table td.rd-gist-read {
2253
- color: var(--op-ink-soft);
2254
- text-wrap: pretty;
2255
- min-width: 0;
2069
+ .atelic-ui .op-part + .rd-gist {
2070
+ margin-top: 0;
2256
2071
  }
2257
2072
  .atelic-ui .rd-beat {
2258
2073
  margin-top: 20px;
@@ -2366,22 +2181,6 @@
2366
2181
  .atelic-ui .rd-doc .op-shots-row[data-count="3"] {
2367
2182
  max-width: none;
2368
2183
  }
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
2184
  /* A swatch as a table cell: the chart key's box, keying the row in its tone, in a column marked fit. */
2386
2185
  .atelic-ui .op-table .chart-swatch,
2387
2186
  .atelic-ui .op-findings .callout-body .chart-swatch {
@@ -2407,10 +2206,12 @@
2407
2206
  font-size: 16.5px;
2408
2207
  color: var(--op-ink-soft);
2409
2208
  }
2209
+ /* the steps: a large numeral in the accent, the title on its own line,
2210
+ the body under it, and the cards it draws on in a small line beneath */
2410
2211
  .atelic-ui .rd-steps {
2411
2212
  list-style: none;
2412
2213
  counter-reset: rd-step;
2413
- margin: 12px 0 0;
2214
+ margin: 14px 0 0;
2414
2215
  padding: 0;
2415
2216
  background: var(--op-card);
2416
2217
  border: 1px solid var(--op-rule);
@@ -2420,38 +2221,66 @@
2420
2221
  .atelic-ui .rd-step {
2421
2222
  counter-increment: rd-step;
2422
2223
  display: grid;
2423
- grid-template-columns: auto minmax(0, 1fr);
2224
+ grid-template-columns: 44px minmax(0, 1fr);
2424
2225
  column-gap: 12px;
2425
- padding: 14px 16px;
2226
+ padding: 18px 20px 18px 18px;
2426
2227
  }
2427
2228
  .atelic-ui .rd-step + .rd-step {
2428
2229
  border-top: 1px solid var(--op-rule);
2429
2230
  }
2430
2231
  .atelic-ui .rd-step::before {
2431
- content: counter(rd-step);
2432
- display: inline-grid;
2433
- place-items: center;
2434
- width: 28px;
2435
- height: 28px;
2436
- border: 1.5px solid var(--op-accent);
2437
- border-radius: 999px;
2232
+ content: counter(rd-step, decimal-leading-zero);
2233
+ margin-top: 1px;
2234
+ font-family: var(--font-mono);
2235
+ font-size: 22px;
2236
+ font-weight: 500;
2237
+ line-height: 1.1;
2238
+ letter-spacing: -0.02em;
2438
2239
  color: var(--op-accent);
2439
- font-size: 14px;
2440
- font-weight: 700;
2441
- line-height: 1;
2240
+ font-variant-numeric: tabular-nums;
2442
2241
  }
2443
- .atelic-ui .rd-step p {
2444
- margin: 3px 0 0;
2445
- color: var(--op-ink-soft);
2446
- text-wrap: pretty;
2242
+ .atelic-ui .rd-step-body {
2243
+ min-width: 0;
2244
+ }
2245
+ .atelic-ui .rd-step-head {
2246
+ margin: 0;
2447
2247
  }
2448
2248
  .atelic-ui .rd-step-title {
2249
+ font-size: 17px;
2449
2250
  font-weight: 650;
2251
+ letter-spacing: -0.015em;
2252
+ line-height: 1.3;
2450
2253
  color: var(--op-ink);
2451
2254
  }
2255
+ .atelic-ui .rd-step-body > p + p {
2256
+ margin: 5px 0 0;
2257
+ font-size: 15.5px;
2258
+ line-height: 1.5;
2259
+ color: var(--op-ink-soft);
2260
+ text-wrap: pretty;
2261
+ }
2262
+ .atelic-ui .rd-step-body > .rd-step-cards {
2263
+ display: flex;
2264
+ flex-wrap: wrap;
2265
+ gap: 4px 18px;
2266
+ margin-top: 9px;
2267
+ font-family: var(--font-mono);
2268
+ font-size: 11px;
2269
+ font-weight: 500;
2270
+ letter-spacing: 0.1em;
2271
+ text-transform: uppercase;
2272
+ }
2273
+ .atelic-ui .rd-step-cards a {
2274
+ color: var(--op-accent);
2275
+ text-decoration: none;
2276
+ }
2277
+ .atelic-ui .rd-step-cards a:hover {
2278
+ text-decoration: underline;
2279
+ text-underline-offset: 3px;
2280
+ }
2452
2281
  .atelic-ui .rd-step-who {
2453
2282
  display: inline-block;
2454
- margin-left: 2px;
2283
+ margin-left: 8px;
2455
2284
  padding: 1px 8px;
2456
2285
  border: 1px solid var(--neutral-200);
2457
2286
  border-radius: 999px;
@@ -2474,13 +2303,6 @@
2474
2303
 
2475
2304
  /* ---- wide and on paper: the pictures beside each other ---- */
2476
2305
  @media print, (min-width: 721px) {
2477
- .atelic-ui .rd-gist-table tbody th[scope="row"],
2478
- .atelic-ui .rd-gist-table td,
2479
- .atelic-ui .rd-gist-table thead th,
2480
- .atelic-ui .rd-gist-tier th {
2481
- padding-left: 20px;
2482
- padding-right: 16px;
2483
- }
2484
2306
  .atelic-ui .rd-changes-table {
2485
2307
  display: table;
2486
2308
  table-layout: fixed;
@@ -2546,12 +2368,10 @@
2546
2368
  /* ---- print: the writeup's rules carry the page; these hold the read's
2547
2369
  own blocks whole ---- */
2548
2370
  @media print {
2549
- .atelic-ui .rd-gist-card,
2550
2371
  .atelic-ui .rd-steps {
2551
2372
  box-shadow: none;
2552
2373
  border-color: var(--neutral-200);
2553
2374
  }
2554
- .atelic-ui .rd-gist-table tr,
2555
2375
  .atelic-ui .rd-changes-table tr,
2556
2376
  .atelic-ui .rd-checks li,
2557
2377
  .atelic-ui .rd-closing,