@atelic-action/artifact 4.2.0 → 5.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.
@@ -5,9 +5,7 @@ import {
5
5
  newTabProps,
6
6
  Runs,
7
7
  Shot,
8
- SignOff,
9
8
  type WriteupGrade,
10
- type WriteupPerson,
11
9
  type WriteupShot,
12
10
  } from "@atelic-action/ui/artifact";
13
11
  import {
@@ -18,7 +16,6 @@ import {
18
16
  type ColumnChartGroup,
19
17
  DataTable,
20
18
  Disclosure,
21
- ReportCard,
22
19
  StackedBar,
23
20
  type StackedBarSegment,
24
21
  type StatItem,
@@ -31,28 +28,25 @@ import {
31
28
  import { ArrowUpRight, CircleAlert, CircleCheck, CircleX, Star } from "lucide-react";
32
29
  import type { ReactNode } from "react";
33
30
 
34
- export type { WriteupGrade, WriteupPerson, WriteupShot } from "@atelic-action/ui/artifact";
35
-
36
31
  /**
37
- * The writeup: the graded document an owner gets. It is a web page first: an
38
- * overview that links into every section, then the three tiers, each section
39
- * a card of small blocks and a line of sources that open the tools the
40
- * reading came from. Print is the same page with the chrome dropped and the
41
- * type brought down, flowing across letter pages; nothing is clipped.
32
+ * The section card: the trade name linked to its definition, the one liner,
33
+ * the grade, the why line, the blocks, and the sources at the foot. The read
34
+ * renders every section through it, handing what the card itself does not
35
+ * know (today and after, what we recommend) as `extra`, set after the blocks
36
+ * and before the sources.
42
37
  *
43
38
  * Every block is a component from @atelic-action/ui (the checklist, the
44
39
  * sortable table, the charts, the stat row, the callout, the disclosure);
45
- * this file owns only the document: the overview, the tiers, the section
46
- * card, the markets table, and the close. Its skeleton is the practice's
47
- * FOOTPRINT.md, and what it says follows the pitch surface rules
40
+ * this file owns only the card around them and the shapes a block's content
41
+ * is written in. What it says follows the pitch surface rules
48
42
  * (Brand/voice.md): the trade's term linked to its definition, or plain
49
43
  * words, and no term an owner would have to look up.
50
44
  */
51
45
 
52
46
  /** How a mark, a bar segment, or a zone reads; the shared package's tones. */
53
- export type WriteupTone = Tone;
47
+ export type ReadTone = Tone;
54
48
 
55
- export interface WriteupBullet {
49
+ export interface ReadBullet {
56
50
  /** The point, in a full sentence. The icon sits beside it. */
57
51
  lead: string;
58
52
  /** The one explainer that says why it matters: a node, or a paragraph of runs when the content arrives as JSON. */
@@ -60,19 +54,19 @@ export interface WriteupBullet {
60
54
  /** A win the owner already has: marked good. Everything else reads as needing work. */
61
55
  win?: boolean;
62
56
  /** Overrides the tone, e.g. "info" for something not yet read. */
63
- tone?: WriteupTone;
57
+ tone?: ReadTone;
64
58
  /** Sets the finding apart on a tinted ground, for something missing or owed. */
65
59
  box?: boolean;
66
60
  }
67
61
 
68
62
  /** A place a reading came from: the tool, opened in a new tab when it has an address. */
69
- export interface WriteupSource {
63
+ export interface ReadSource {
70
64
  label: string;
71
65
  href?: string;
72
66
  }
73
67
 
74
68
  /** A yes or no cell: the icon alone, with the text read to a screen reader. A figure a reader needs (a rank, a count) is a column of its own. */
75
- export interface WriteupMark {
69
+ export interface ReadMark {
76
70
  mark: "good" | "warn" | "bad" | "none";
77
71
  /** What the mark means, e.g. "Loads" or "Page Not Found"; always read to a screen reader. */
78
72
  text: string;
@@ -83,14 +77,14 @@ export interface WriteupMark {
83
77
  }
84
78
 
85
79
  /** A cell whose display is not what it sorts by, e.g. "4.8" beside a star. */
86
- export interface WriteupSorted {
80
+ export interface ReadSorted {
87
81
  display: ReactNode;
88
82
  sort: number | string;
89
83
  }
90
84
 
91
- export type WriteupCell = ReactNode | WriteupMark | WriteupSorted;
85
+ export type ReadCell = ReactNode | ReadMark | ReadSorted;
92
86
 
93
- function isMark(cell: WriteupCell): cell is WriteupMark {
87
+ function isMark(cell: ReadCell): cell is ReadMark {
94
88
  return typeof cell === "object" && cell !== null && "mark" in cell;
95
89
  }
96
90
 
@@ -101,13 +95,13 @@ interface BlockBase {
101
95
  }
102
96
 
103
97
  /** A short list of pass or fail checks, the basics a section rests on. */
104
- export interface WriteupChecks extends BlockBase {
98
+ export interface ReadChecks extends BlockBase {
105
99
  kind: "checks";
106
100
  items: ChecklistItem[];
107
101
  }
108
102
 
109
103
  /** A table: the first column names the row, the rest are short facts or marks. */
110
- export interface WriteupTable extends BlockBase {
104
+ export interface ReadTable extends BlockBase {
111
105
  kind: "table";
112
106
  columns: Array<{
113
107
  label: string;
@@ -119,19 +113,19 @@ export interface WriteupTable extends BlockBase {
119
113
  /** The column hugs its content, for a swatch or a mark. */
120
114
  fit?: boolean;
121
115
  }>;
122
- rows: Array<{ cells: WriteupCell[]; tone?: WriteupTone }>;
116
+ rows: Array<{ cells: ReadCell[]; tone?: ReadTone }>;
123
117
  /** The sort the table opens in. */
124
118
  sort?: { column: number; direction: "asc" | "desc" };
125
119
  }
126
120
 
127
121
  /** One stacked bar: the parts of a whole, with a key. */
128
- export interface WriteupBar extends BlockBase {
122
+ export interface ReadBar extends BlockBase {
129
123
  kind: "bar";
130
124
  segments: StackedBarSegment[];
131
125
  }
132
126
 
133
127
  /** A scale with zones and one line per reading. */
134
- export interface WriteupScale extends BlockBase {
128
+ export interface ReadScale extends BlockBase {
135
129
  kind: "scale";
136
130
  max: number;
137
131
  zones: ThresholdZone[];
@@ -140,59 +134,59 @@ export interface WriteupScale extends BlockBase {
140
134
  }
141
135
 
142
136
  /** A few numbers worth reading first, each with its period and its comparisons. */
143
- export interface WriteupStats extends BlockBase {
137
+ export interface ReadStats extends BlockBase {
144
138
  kind: "stats";
145
139
  items: StatItem[];
146
140
  }
147
141
 
148
142
  /** A column chart: a bar a period, the periods in labeled groups. */
149
- export interface WriteupColumns extends BlockBase {
143
+ export interface ReadColumns extends BlockBase {
150
144
  kind: "columns";
151
145
  groups: ColumnChartGroup[];
152
- legend?: Array<{ label: string; tone: WriteupTone }>;
146
+ legend?: Array<{ label: string; tone: ReadTone }>;
153
147
  }
154
148
 
155
149
  /** Findings, each an icon beside its point with the explainer under it, and the owner's screen beside them. */
156
- export interface WriteupFindings {
150
+ export interface ReadFindings {
157
151
  kind: "findings";
158
- items: WriteupBullet[];
152
+ items: ReadBullet[];
159
153
  shot?: WriteupShot;
160
154
  shot2?: WriteupShot;
161
155
  }
162
156
 
163
157
  /** The owner's own screens, side by side at one size. */
164
- export interface WriteupShots {
158
+ export interface ReadShots {
165
159
  kind: "shots";
166
160
  title?: string;
167
161
  shots: WriteupShot[];
168
162
  }
169
163
 
170
164
  /** Blocks that can sit inside a fold (a `more` block). */
171
- export type WriteupInnerBlock =
172
- | WriteupChecks
173
- | WriteupTable
174
- | WriteupBar
175
- | WriteupScale
176
- | WriteupFindings
177
- | WriteupStats
178
- | WriteupColumns
179
- | WriteupShots;
165
+ export type ReadInnerBlock =
166
+ | ReadChecks
167
+ | ReadTable
168
+ | ReadBar
169
+ | ReadScale
170
+ | ReadFindings
171
+ | ReadStats
172
+ | ReadColumns
173
+ | ReadShots;
180
174
 
181
175
  /**
182
176
  * A fold, the native Disclosure: the label says what is inside and the
183
177
  * summary says what it shows, so the detail needs no explaining once open.
184
178
  */
185
- export interface WriteupMore {
179
+ export interface ReadMore {
186
180
  kind: "more";
187
181
  label: string;
188
182
  summary?: ReactNode;
189
- blocks: WriteupInnerBlock[];
183
+ blocks: ReadInnerBlock[];
190
184
  }
191
185
 
192
- export type WriteupTopicBlock = WriteupInnerBlock | WriteupMore;
186
+ export type ReadBlock = ReadInnerBlock | ReadMore;
193
187
 
194
- export interface WriteupTopic {
195
- kind: "topic";
188
+ /** The section card's own content: what the card knows how to draw on its own. */
189
+ export interface ReadTopic {
196
190
  id: string;
197
191
  /** The discipline's trade name, e.g. "Local SEO" or "Core Web Vitals". */
198
192
  title: string;
@@ -204,126 +198,27 @@ export interface WriteupTopic {
204
198
  oneLiner: string;
205
199
  /** The section's grade, beside the heading. */
206
200
  grade?: WriteupGrade;
207
- /** Why it earned that letter: the rule it met, in a sentence. The read sets its summary here. */
201
+ /** Why it earned that letter: the summary of what we saw, in a sentence or two. */
208
202
  why?: ReactNode;
209
203
  /** The word before the grade on the why line; defaults to "Why". */
210
204
  whyLabel?: string;
211
205
  /** What a screen reader hears before the grade's letter; defaults to "Grade". */
212
206
  gradeWord?: string;
213
207
  /** The section's blocks, in order. */
214
- blocks?: WriteupTopicBlock[];
215
- /** Findings without blocks: rendered as one findings block with the shots beside it. */
216
- bullets?: WriteupBullet[];
217
- shot?: WriteupShot;
218
- shot2?: WriteupShot;
219
- shotBrief?: string;
220
- /** The topic carries no picture by design. */
221
- noShot?: boolean;
208
+ blocks?: ReadBlock[];
222
209
  /** The tools the readings came from, linked. */
223
- sources?: WriteupSource[];
224
- /** The label over the sources, e.g. "Where this comes from". */
210
+ sources?: ReadSource[];
211
+ /** The label over the sources, e.g. "See it yourself". */
225
212
  sourcesLabel?: string;
226
213
  }
227
214
 
228
- export interface WriteupPart {
229
- kind: "part";
230
- /** The anchor a header stop points at, e.g. "health". */
231
- id?: string;
232
- /** The tier's name, e.g. "Health". */
233
- title: string;
234
- /** The question the tier answers, in the owner's words. */
235
- lead: string;
236
- }
237
-
238
- export interface WriteupSummaryRow {
239
- /** The tier, e.g. "Health". */
240
- tier: string;
241
- /** The section's trade name, matching a topic title. */
242
- section: string;
243
- /** The topic's anchor, e.g. "#technical-seo"; the row links to it. */
244
- href?: string;
245
- grade: WriteupGrade;
246
- /** The grade in the earlier snapshot, on a later writeup: what moved. */
247
- before?: WriteupGrade;
248
- /** The read behind the grade, in the owner's words. */
249
- read: ReactNode;
250
- }
251
-
252
- /** The overview the writeup opens on: every section, its grade, and one plain read. */
253
- export interface WriteupSummary {
254
- kind: "summary";
255
- id?: string;
256
- title?: string;
257
- rows: WriteupSummaryRow[];
258
- /** What each letter means, in a few words each. */
259
- legend?: Array<{ grade: WriteupGrade; label: string }>;
260
- /** What a screen reader calls the earlier letter, e.g. "June 2026"; defaults to "Before". */
261
- beforeLabel?: string;
262
- }
263
-
264
- /** The paid facts line: what is rented, never graded (FOOTPRINT.md, Paid). */
265
- export interface WriteupRented {
266
- kind: "rented";
267
- label?: string;
268
- text: ReactNode;
269
- }
270
-
271
- export interface WriteupMarketRow {
272
- market: string;
273
- place?: string;
274
- /** One grade per column, in column order; null draws a quiet dash for a column not read. */
275
- grades: Array<WriteupGrade | null>;
276
- read: ReactNode;
277
- }
278
-
279
- /** The markets table: one row per market, one letter per column, the read beside it. */
280
- export interface WriteupMarkets {
281
- kind: "markets";
282
- title: string;
283
- lead?: string;
284
- columns: string[];
285
- rows: WriteupMarketRow[];
286
- }
287
-
288
- export type WriteupBlock =
289
- | WriteupPart
290
- | WriteupTopic
291
- | WriteupSummary
292
- | WriteupRented
293
- | WriteupMarkets;
294
-
295
- export interface WriteupSheet {
296
- id: string;
297
- blocks: WriteupBlock[];
298
- }
299
-
300
- export interface WriteupProps {
301
- /** The sender's wordmark text, e.g. "atelic"; drawn with the orange run. Print only. */
302
- sender: { name: string; href?: string };
303
- /** The recipient's logo, lockup right of the sender. Print only. */
304
- recipient: { logoSrc: string; logoAlt: string };
305
- preparedFor: string;
306
- date: string;
307
- title: string;
308
- /** One or two sentences under the title: what this is and where it was read from. */
309
- intro?: ReactNode;
310
- /** Groups of blocks. They run together on screen and in print. */
311
- sheets: WriteupSheet[];
312
- close: {
313
- signOff: string;
314
- person: WriteupPerson;
315
- /** The pointer to the hosted page, on the printed copy only. */
316
- twin?: { line: string; domain: string; qrSrc: string };
317
- };
318
- }
319
-
320
215
  const toneIcon = {
321
216
  good: CircleCheck,
322
217
  bad: CircleX,
323
218
  warn: CircleAlert,
324
219
  } as const;
325
220
 
326
- function Mark({ cell }: { cell: WriteupMark }) {
221
+ function Mark({ cell }: { cell: ReadMark }) {
327
222
  if (cell.mark === "none") {
328
223
  return (
329
224
  <span className="op-mark-cell" data-tone="muted">
@@ -343,7 +238,7 @@ function Mark({ cell }: { cell: WriteupMark }) {
343
238
  }
344
239
 
345
240
  /** A star rating as a table cell: the figure beside a star, sorted by the figure. */
346
- export function ratingCell(rating: number, tone?: WriteupTone): WriteupSorted {
241
+ export function ratingCell(rating: number, tone?: ReadTone): ReadSorted {
347
242
  return {
348
243
  sort: rating,
349
244
  display: (
@@ -381,7 +276,7 @@ function Block({
381
276
  );
382
277
  }
383
278
 
384
- function Table({ block }: { block: WriteupTable }) {
279
+ function Table({ block }: { block: ReadTable }) {
385
280
  const rows = block.rows.map((row) => ({
386
281
  tone: row.tone,
387
282
  cells: row.cells.map((cell) =>
@@ -398,7 +293,7 @@ function Table({ block }: { block: WriteupTable }) {
398
293
  }
399
294
 
400
295
  /** A string or runs of text and links, the shape JSON content arrives in; a node of elements is anything else. */
401
- function isRuns(value: unknown): value is ArtifactParagraph {
296
+ export function isRuns(value: unknown): value is ArtifactParagraph {
402
297
  if (typeof value === "string") return true;
403
298
  return (
404
299
  Array.isArray(value) &&
@@ -416,8 +311,8 @@ function bodyNode(body: ReactNode | ArtifactParagraph | undefined): ReactNode {
416
311
  return isRuns(body) ? <Runs paragraph={body} /> : body;
417
312
  }
418
313
 
419
- function Findings({ block, brief }: { block: WriteupFindings; brief?: string }) {
420
- const hasShots = Boolean(block.shot || brief);
314
+ function Findings({ block }: { block: ReadFindings }) {
315
+ const hasShots = Boolean(block.shot);
421
316
  return (
422
317
  <div className={`op-block op-findings${hasShots ? " has-shots" : ""}`}>
423
318
  <div className="op-findings-list">
@@ -435,25 +330,17 @@ function Findings({ block, brief }: { block: WriteupFindings; brief?: string })
435
330
  );
436
331
  })}
437
332
  </div>
438
- {hasShots && (
333
+ {block.shot && (
439
334
  <div className="op-shots">
440
- {block.shot ? (
441
- <>
442
- <Shot shot={block.shot} />
443
- {block.shot2 && <Shot shot={block.shot2} />}
444
- </>
445
- ) : (
446
- <figure className="op-shot">
447
- <span className="op-shot-brief">{brief}</span>
448
- </figure>
449
- )}
335
+ <Shot shot={block.shot} />
336
+ {block.shot2 && <Shot shot={block.shot2} />}
450
337
  </div>
451
338
  )}
452
339
  </div>
453
340
  );
454
341
  }
455
342
 
456
- function Shots({ block }: { block: WriteupShots }) {
343
+ function Shots({ block }: { block: ReadShots }) {
457
344
  return (
458
345
  <Block className="op-shots-block" block={block}>
459
346
  <div className="op-shots-row" data-count={Math.min(block.shots.length, 3)}>
@@ -465,7 +352,7 @@ function Shots({ block }: { block: WriteupShots }) {
465
352
  );
466
353
  }
467
354
 
468
- function More({ block }: { block: WriteupMore }) {
355
+ function More({ block }: { block: ReadMore }) {
469
356
  return (
470
357
  <Disclosure className="op-block op-more" label={block.label} summary={block.summary}>
471
358
  <div className="op-more-body">
@@ -478,7 +365,7 @@ function More({ block }: { block: WriteupMore }) {
478
365
  );
479
366
  }
480
367
 
481
- function TopicBlock({ block }: { block: WriteupTopicBlock }) {
368
+ function TopicBlock({ block }: { block: ReadBlock }) {
482
369
  switch (block.kind) {
483
370
  case "checks":
484
371
  return (
@@ -527,17 +414,10 @@ function TopicBlock({ block }: { block: WriteupTopicBlock }) {
527
414
  }
528
415
 
529
416
  /**
530
- * The section card: the trade name linked to its definition, the one liner,
531
- * the why line, the grade, the blocks, and the sources at the foot. The read
532
- * renders its sections through it too, handing anything of its own (today and
533
- * after, what we recommend) as `extra`, set after the blocks and before the
534
- * sources, so the two documents share one layout.
417
+ * The section card. `extra` is set after the blocks and before the sources,
418
+ * so a document's own beats sit inside the card with the rest.
535
419
  */
536
- export function Topic({ topic, extra }: { topic: WriteupTopic; extra?: ReactNode }) {
537
- const legacy: WriteupFindings | null = topic.bullets
538
- ? { kind: "findings", items: topic.bullets, shot: topic.shot, shot2: topic.shot2 }
539
- : null;
540
- const brief = topic.noShot || topic.shot ? undefined : (topic.shotBrief ?? "Screenshot");
420
+ export function Topic({ topic, extra }: { topic: ReadTopic; extra?: ReactNode }) {
541
421
  return (
542
422
  <section className="op-topic" id={topic.id}>
543
423
  <header className="op-topic-head">
@@ -582,7 +462,6 @@ export function Topic({ topic, extra }: { topic: WriteupTopic; extra?: ReactNode
582
462
  // biome-ignore lint/suspicious/noArrayIndexKey: blocks are static content in a fixed order
583
463
  <TopicBlock block={block} key={i} />
584
464
  ))}
585
- {legacy && <Findings block={legacy} brief={brief} />}
586
465
  {extra}
587
466
  </div>
588
467
  {topic.sources && topic.sources.length > 0 && (
@@ -608,185 +487,3 @@ export function Topic({ topic, extra }: { topic: WriteupTopic; extra?: ReactNode
608
487
  </section>
609
488
  );
610
489
  }
611
-
612
- function Summary({ summary }: { summary: WriteupSummary }) {
613
- const groups: Array<{ title: string; rows: WriteupSummaryRow[] }> = [];
614
- for (const row of summary.rows) {
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] });
618
- }
619
- return (
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
- />
637
- );
638
- }
639
-
640
- function Markets({ markets }: { markets: WriteupMarkets }) {
641
- return (
642
- <section className="op-topic op-markets">
643
- <header className="op-topic-head">
644
- <div>
645
- <h3>{markets.title}</h3>
646
- {markets.lead && <p className="op-def">{markets.lead}</p>}
647
- </div>
648
- </header>
649
- <table className="op-markets-table">
650
- <thead>
651
- <tr>
652
- <th scope="col" className="op-markets-market">
653
- <span className="op-sr-only">Market</span>
654
- </th>
655
- {markets.columns.map((column) => (
656
- <th scope="col" className="op-markets-col" key={column}>
657
- {column}
658
- </th>
659
- ))}
660
- <th scope="col" className="op-markets-read">
661
- <span className="op-sr-only">Read</span>
662
- </th>
663
- </tr>
664
- </thead>
665
- <tbody>
666
- {markets.rows.map((row) => (
667
- <tr key={row.market}>
668
- <th scope="row" className="op-markets-market">
669
- {row.market}
670
- {row.place && <span className="op-markets-place">{row.place}</span>}
671
- </th>
672
- {markets.columns.map((column, i) => {
673
- const grade = row.grades[i] ?? null;
674
- return (
675
- <td className="op-markets-grade" key={column}>
676
- <span className="op-markets-label" aria-hidden="true">
677
- {column}
678
- </span>
679
- {grade ? (
680
- <>
681
- <span className="op-sr-only">{`${column}, grade ${gradeLabel[grade]}`}</span>
682
- <span aria-hidden="true">
683
- <GradeChip grade={grade} />
684
- </span>
685
- </>
686
- ) : (
687
- <>
688
- <span className="op-sr-only">{`${column}, not read`}</span>
689
- <span className="op-dash" aria-hidden="true" />
690
- </>
691
- )}
692
- </td>
693
- );
694
- })}
695
- <td className="op-markets-read">{row.read}</td>
696
- </tr>
697
- ))}
698
- </tbody>
699
- </table>
700
- </section>
701
- );
702
- }
703
-
704
- function Rented({ rented }: { rented: WriteupRented }) {
705
- return (
706
- <p className="op-rented">
707
- <span className="op-rented-label">{rented.label ?? "Rented"}</span>
708
- <span className="op-rented-text">{rented.text}</span>
709
- </p>
710
- );
711
- }
712
-
713
- function Part({ part }: { part: WriteupPart }) {
714
- return (
715
- <div className="op-part" id={part.id}>
716
- <h2>{part.title}</h2>
717
- <p>{part.lead}</p>
718
- </div>
719
- );
720
- }
721
-
722
- export function Writeup({
723
- sender,
724
- recipient,
725
- preparedFor,
726
- date,
727
- title,
728
- intro,
729
- sheets,
730
- close,
731
- }: WriteupProps) {
732
- return (
733
- <div className="op">
734
- <div className="op-doc">
735
- <header className="op-head">
736
- <div className="op-top">
737
- <span className="op-mark">
738
- {sender.href ? <a href={sender.href}>{sender.name}</a> : sender.name}
739
- <span className="op-run" aria-hidden="true" />
740
- </span>
741
- <span className="op-x" aria-hidden="true">
742
- ×
743
- </span>
744
- <img className="op-recipient" src={recipient.logoSrc} alt={recipient.logoAlt} />
745
- </div>
746
- <p className="op-meta">
747
- Prepared for <b>{preparedFor}</b>
748
- <span className="op-meta-sep" aria-hidden="true">
749
- {" · "}
750
- </span>
751
- <span className="op-meta-date">{date}</span>
752
- </p>
753
- <h1>{title}</h1>
754
- {intro && <p className="op-intro">{intro}</p>}
755
- </header>
756
- {sheets.map((sheet) => (
757
- <div className="op-sheet" id={sheet.id} key={sheet.id}>
758
- {sheet.blocks.map((block) =>
759
- block.kind === "part" ? (
760
- <Part key={block.title} part={block} />
761
- ) : block.kind === "summary" ? (
762
- <Summary key="summary" summary={block} />
763
- ) : block.kind === "rented" ? (
764
- <Rented key="rented" rented={block} />
765
- ) : block.kind === "markets" ? (
766
- <Markets key={`markets-${block.title}`} markets={block} />
767
- ) : (
768
- <Topic key={block.id} topic={block} />
769
- ),
770
- )}
771
- </div>
772
- ))}
773
- <footer className="op-close">
774
- <SignOff signOff={close.signOff} person={close.person} />
775
- {close.twin && (
776
- <div className="op-twin">
777
- <div className="op-twin-text">
778
- {close.twin.line}
779
- <b>{close.twin.domain}</b>
780
- </div>
781
- <img
782
- className="op-qr"
783
- src={close.twin.qrSrc}
784
- alt={`QR code for ${close.twin.domain}`}
785
- />
786
- </div>
787
- )}
788
- </footer>
789
- </div>
790
- </div>
791
- );
792
- }