@atelic-action/ui 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 +4 -1
- package/package.json +1 -1
- package/src/artifact/Note.tsx +9 -39
- package/src/artifact/Paragraph.tsx +65 -0
- package/src/artifact/Read.tsx +406 -0
- package/src/artifact/index.ts +1 -0
- package/src/styles/artifact.css +287 -2
package/README.md
CHANGED
|
@@ -33,6 +33,7 @@ Every export, grouped by the entry it is imported from. The gallery column links
|
|
|
33
33
|
| `readCookie`, `Tone` | `@atelic-action/ui/components` | The cookie reader the watermark uses, and the tone type | none |
|
|
34
34
|
| `Button`, `Chip`, `Eyebrow`, `Lead`, `SectionHeading` | `@atelic-action/ui/primitives` | The small pieces a page is set in | [#primitives](https://ui.atelic.me/#primitives) |
|
|
35
35
|
| `Writeup`, `ratingCell` | `@atelic-action/ui/artifact` | The graded document an owner gets, and its star rating table cell | [#writeup](https://ui.atelic.me/#writeup) |
|
|
36
|
+
| `Read`, `readLabels` | `@atelic-action/ui/artifact` | The read: a customer's baseline grades explained by cause at the end of the first week, with the plan in order, and its default fixed words | [#read](https://ui.atelic.me/#read) |
|
|
36
37
|
| `Report` | `@atelic-action/ui/artifact` | The monthly page an Operate client gets | [#report](https://ui.atelic.me/#report) |
|
|
37
38
|
| `Note` | `@atelic-action/ui/artifact` | The first touch page, for a business with no published email | [#note](https://ui.atelic.me/#note) |
|
|
38
39
|
| `Gate`, `normalizeEmail` | `@atelic-action/ui/artifact` | The sealed document, opened in the browser by the reader's own address | [#gate](https://ui.atelic.me/#gate) |
|
|
@@ -331,7 +332,7 @@ The two pieces a reader sees are ordinary components, in `@atelic-action/ui/comp
|
|
|
331
332
|
|
|
332
333
|
`@atelic-action/ui/artifact` is everything the artifact template shared across its cuts, so a fix reaches every business's documents with one version bump:
|
|
333
334
|
|
|
334
|
-
- **The documents:** `Writeup`, `Report`, `Note`, and `Gate` (the sealed document, with its `GatePayload` type; a reader's address is the key, so a payload never lists it, and the component identifies a reader to HubSpot by the address they typed), plus the `GradeChip` and `SignOff` they share, and every content type they take (`WriteupProps`, `ReportProps`, `NoteProps`, `GateProps`, and the rest).
|
|
335
|
+
- **The documents:** `Writeup`, `Read`, `Report`, `Note`, and `Gate` (the sealed document, with its `GatePayload` type; a reader's address is the key, so a payload never lists it, and the component identifies a reader to HubSpot by the address they typed), plus the `GradeChip` and `SignOff` they share, and every content type they take (`WriteupProps`, `ReadProps`, `ReportProps`, `NoteProps`, `GateProps`, and the rest).
|
|
335
336
|
- **The shell:** `ArtifactShell`, the `.atelic-ui` scope with the skip link, the dark header, the credit band, and the watermark around a page.
|
|
336
337
|
- **The contract and the head:** the `ArtifactConfig` types, and `buildPageHead` and `canonicalUrl` with their `PageMeta` type.
|
|
337
338
|
- **Who opened it:** `mintToken`, `sealReader`, `openReader`, `readerId`, `readerLink`, and `whoIsReading`. The same six are also built and exported alone as `@atelic-action/ui/artifact/readers`, because a Playwright spec and a script run under Node, which compiles nothing inside `node_modules`.
|
|
@@ -352,6 +353,8 @@ export function ArtifactShell(props: Omit<ArtifactShellProps, "currentPath">) {
|
|
|
352
353
|
|
|
353
354
|
**The note** is the first touch for a business whose site publishes no email address, served at `/notes` and wrapped in `ArtifactShell` exactly as the writeup is. It is the writeup's sibling, not a variant: a title and a dateline, the opener, two lanes of findings (what the customer sees, what sits under the hood), the steps the work would start with, and the close over the shared `SignOff`. Every prop is plain data, so a routine can write a note as JSON; a paragraph is a string or an array of strings and `{ text, href }` links, and the sign off person's `role` is the one field that also takes a node. A finding is a `Callout` with its `WriteupShot` under it. Its column is the writeup's 920px, centered under the header with the header's `--gutter`, and its text runs the column's full width. A finding sets its screen under its words on a phone and beside them, on the right, when wide and on paper. It reads on a phone first, stacks the two lanes as separate sections top to bottom at every width and on paper, never side by side, and prints to one letter sheet under the writeup's `@page`.
|
|
354
355
|
|
|
356
|
+
**The read** is what a customer gets at the end of an engagement's first week, before anything changes: the baseline's grades explained by cause (FOOTPRINT.md, What the Owner Gets). It is the writeup's sibling and wears the writeup's own `.op` root, so its column, head, grade chip and key, tier headings, section cards, screens, sources, close, and print rules are the writeup's; `artifact.css` adds only its `.rd` classes. Top to bottom: a title, the dateline, and a lede of two or three sentences; the gist table (Section, Grade, The Gist), drawn from the tiers so it can never disagree with the body, each section's name linking to its card and a section with no grade showing a quiet dash; then each tier's sections, each headed by its question with its trade name over it and its grade beside it, in three beats (what we found, with an optional short list of examples and any proof pictures in a row under their captions; why, one sentence set apart; what changes) and the tools it was read from; then the plan, numbered, each step a bold title and a sentence or two, with `who` naming a step that is the owner's own; then one closing line over the shared `SignOff`. A later tier (Conversion, once the analytics access lands) is one more entry in `tiers` and appears in the table and the body alike, with no change to the props, and a tier with no sections yet is left out of both. Every prop is plain data, the note's paragraph shape (a string, or runs of text and `{ text, href }` links), so a routine can write a read as JSON. The fixed words (the column heads, the beat labels, the plan's heading) are `readLabels`, and `labels` overrides any of them. A site wraps it in `ArtifactShell` exactly as it wraps the writeup; it sets no `@page`, so the writeup's stays the one that wins.
|
|
357
|
+
|
|
355
358
|
**The sealed document's classes.** `Gate` renders the opened document as the HTML it was sealed from, so its markup is the content's, and `artifact.css` styles what that HTML may wear inside `.gate-content`: the tier row (`.gate-tiers` of `.gate-tier` cards, `.is-picked` on the recommended one, `.gate-tier-name`, `.gate-tier-price`, `.gate-tier-note`, and the `.gate-tier-math` sum column of `.gate-tier-was`, `.gate-tier-credit`, `.gate-tier-mathlabel`, and the price), `ul.icon-list` for a list led by inline lucide icons (sized for the card inside a tier), `ul.gate-checks`, and, from 1.0.0 (ATE-599, pulled up from the Wynkoop and Outdoors Geek hosts), the `.gate-addons` row that seats two `.gate-tier.is-addon` monthly cards side by side under the builds, an inline SVG mark inside `.gate-tier-mathlabel` that stays on the label's line, and the `.gate-footnote` line under the tiers that the mark points at. A folder stylesheet that carried any of these deletes it.
|
|
356
359
|
|
|
357
360
|
The documents import no stylesheet of their own. A site imports `@atelic-action/ui/styles/artifact.css` in its base sheet after `gate.css` and before its `theme.css`, so every page carries the rules whichever route it is. The sheet holds two `@page` rules, the monthly page's and then the writeup's, and the later one wins for every page a site prints; the order inside the sheet is the order the template's build bundled them in and is load bearing.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atelic-action/ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Shared UI for the Atelic templates: site chrome, base styles, the component library, the access gate, and the app shell they install",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
package/src/artifact/Note.tsx
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { Callout } from "../components";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type ArtifactLink,
|
|
4
|
+
type ArtifactParagraph,
|
|
5
|
+
isEmpty,
|
|
6
|
+
ArtifactAnchor as NoteAnchor,
|
|
7
|
+
Paragraph,
|
|
8
|
+
} from "./Paragraph";
|
|
3
9
|
import { Shot, type WriteupShot } from "./Shot";
|
|
4
10
|
import { SignOff, type WriteupPerson } from "./SignOff";
|
|
5
11
|
|
|
@@ -20,13 +26,10 @@ import { SignOff, type WriteupPerson } from "./SignOff";
|
|
|
20
26
|
*/
|
|
21
27
|
|
|
22
28
|
/** A link inside running text: the words the reader clicks and where they go. */
|
|
23
|
-
export
|
|
24
|
-
text: string;
|
|
25
|
-
href: string;
|
|
26
|
-
}
|
|
29
|
+
export type NoteLink = ArtifactLink;
|
|
27
30
|
|
|
28
31
|
/** A paragraph: a plain string, or runs of text and links in order. */
|
|
29
|
-
export type NoteParagraph =
|
|
32
|
+
export type NoteParagraph = ArtifactParagraph;
|
|
30
33
|
|
|
31
34
|
/** One defect and what it costs, with the owner's screen under it when there is one. */
|
|
32
35
|
export interface NoteFinding {
|
|
@@ -86,39 +89,6 @@ export interface NoteProps {
|
|
|
86
89
|
close: NoteClose;
|
|
87
90
|
}
|
|
88
91
|
|
|
89
|
-
/** A link opens in a new tab when it leaves the page, and in place when it is an anchor or a mail link. */
|
|
90
|
-
function NoteAnchor({ href, children }: { href: string; children: string }) {
|
|
91
|
-
return (
|
|
92
|
-
<a href={href} {...newTabProps(href, /^https?:/i.test(href))}>
|
|
93
|
-
{children}
|
|
94
|
-
</a>
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** A paragraph with nothing to read: a blank string, or runs that are all blank strings. A link always counts. */
|
|
99
|
-
function isEmpty(paragraph: NoteParagraph): boolean {
|
|
100
|
-
const runs = typeof paragraph === "string" ? [paragraph] : paragraph;
|
|
101
|
-
return runs.every((run) => typeof run === "string" && run.trim() === "");
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function Paragraph({ paragraph, className }: { paragraph: NoteParagraph; className?: string }) {
|
|
105
|
-
const runs = typeof paragraph === "string" ? [paragraph] : paragraph;
|
|
106
|
-
return (
|
|
107
|
-
<p className={className}>
|
|
108
|
-
{runs.map((run, i) =>
|
|
109
|
-
typeof run === "string" ? (
|
|
110
|
-
run
|
|
111
|
-
) : (
|
|
112
|
-
// biome-ignore lint/suspicious/noArrayIndexKey: runs are static content in a fixed order
|
|
113
|
-
<NoteAnchor href={run.href} key={i}>
|
|
114
|
-
{run.text}
|
|
115
|
-
</NoteAnchor>
|
|
116
|
-
),
|
|
117
|
-
)}
|
|
118
|
-
</p>
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
92
|
function Finding({ finding }: { finding: NoteFinding }) {
|
|
123
93
|
return (
|
|
124
94
|
<li className={finding.shot ? "nt-finding has-shot" : "nt-finding"}>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { newTabProps } from "../lib/newTabProps";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Plain data text for the documents a routine can write as JSON (the note
|
|
5
|
+
* and the read): a paragraph is a string, or runs of text and links in
|
|
6
|
+
* order. A link opens in a new tab when it leaves the page, and in place
|
|
7
|
+
* when it is an anchor or a mail link.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** A link inside running text: the words the reader clicks and where they go. */
|
|
11
|
+
export interface ArtifactLink {
|
|
12
|
+
text: string;
|
|
13
|
+
href: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** A paragraph: a plain string, or runs of text and links in order. */
|
|
17
|
+
export type ArtifactParagraph = string | Array<string | ArtifactLink>;
|
|
18
|
+
|
|
19
|
+
/** A link opens in a new tab when it leaves the page, and in place when it is an anchor or a mail link. */
|
|
20
|
+
export function ArtifactAnchor({ href, children }: { href: string; children: string }) {
|
|
21
|
+
return (
|
|
22
|
+
<a href={href} {...newTabProps(href, /^https?:/i.test(href))}>
|
|
23
|
+
{children}
|
|
24
|
+
</a>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A paragraph with nothing to read: a blank string, or runs that are all blank strings. A link always counts. */
|
|
29
|
+
export function isEmpty(paragraph: ArtifactParagraph): boolean {
|
|
30
|
+
const runs = typeof paragraph === "string" ? [paragraph] : paragraph;
|
|
31
|
+
return runs.every((run) => typeof run === "string" && run.trim() === "");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The runs alone, for a container that is not a paragraph (a list item, a table cell). */
|
|
35
|
+
export function Runs({ paragraph }: { paragraph: ArtifactParagraph }) {
|
|
36
|
+
const runs = typeof paragraph === "string" ? [paragraph] : paragraph;
|
|
37
|
+
return (
|
|
38
|
+
<>
|
|
39
|
+
{runs.map((run, i) =>
|
|
40
|
+
typeof run === "string" ? (
|
|
41
|
+
run
|
|
42
|
+
) : (
|
|
43
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: runs are static content in a fixed order
|
|
44
|
+
<ArtifactAnchor href={run.href} key={i}>
|
|
45
|
+
{run.text}
|
|
46
|
+
</ArtifactAnchor>
|
|
47
|
+
),
|
|
48
|
+
)}
|
|
49
|
+
</>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function Paragraph({
|
|
54
|
+
paragraph,
|
|
55
|
+
className,
|
|
56
|
+
}: {
|
|
57
|
+
paragraph: ArtifactParagraph;
|
|
58
|
+
className?: string;
|
|
59
|
+
}) {
|
|
60
|
+
return (
|
|
61
|
+
<p className={className}>
|
|
62
|
+
<Runs paragraph={paragraph} />
|
|
63
|
+
</p>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { ArrowUpRight } from "lucide-react";
|
|
2
|
+
import { newTabProps } from "../lib/newTabProps";
|
|
3
|
+
import { GradeChip, gradeLabel, type WriteupGrade } from "./GradeChip";
|
|
4
|
+
import { type ArtifactLink, type ArtifactParagraph, isEmpty, Paragraph, Runs } from "./Paragraph";
|
|
5
|
+
import { Shot, type WriteupShot } from "./Shot";
|
|
6
|
+
import { SignOff, type WriteupPerson } from "./SignOff";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The read: what a customer gets at the end of an engagement's first week,
|
|
10
|
+
* before anything changes. Where the writeup says what letter each section
|
|
11
|
+
* earned, the read says why, and what changes. Its shape is the practice's
|
|
12
|
+
* FOOTPRINT.md, under What the Owner Gets: a title and a dated lede; the gist
|
|
13
|
+
* table (every section, its grade, and one line); then the tiers, each
|
|
14
|
+
* section a question in three beats (what we found, why, what changes) with
|
|
15
|
+
* any proof pictures and the tools it was read from; then the plan, in order;
|
|
16
|
+
* then one closing line and the sign off.
|
|
17
|
+
*
|
|
18
|
+
* It is the writeup's sibling, not a variant: it wears the writeup's column,
|
|
19
|
+
* head, grade chip and key, tier heading, section card, screen, sources, and
|
|
20
|
+
* close (the `.op` classes), and adds only what is its own (the `.rd`
|
|
21
|
+
* classes). The gist table is drawn from the tiers, so a tier added later
|
|
22
|
+
* (Conversion, once the analytics access lands) is one more entry in `tiers`
|
|
23
|
+
* and appears in the table and the body alike, with no change to the props.
|
|
24
|
+
* Every prop is plain data, so a routine can write a read as JSON; the one
|
|
25
|
+
* exception is the sign off person's `role`, which takes a node and is
|
|
26
|
+
* equally happy with a string.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** A link inside running text: the words the reader clicks and where they go. */
|
|
30
|
+
export type ReadLink = ArtifactLink;
|
|
31
|
+
|
|
32
|
+
/** A paragraph: a plain string, or runs of text and links in order. */
|
|
33
|
+
export type ReadParagraph = ArtifactParagraph;
|
|
34
|
+
|
|
35
|
+
/** A place a reading came from: the tool, opened in a new tab when it has an address. */
|
|
36
|
+
export interface ReadSource {
|
|
37
|
+
label: string;
|
|
38
|
+
href?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** One section of a tier: a question in three beats. */
|
|
42
|
+
export interface ReadSection {
|
|
43
|
+
/** The anchor the gist table links to, e.g. "technical-seo". */
|
|
44
|
+
id: string;
|
|
45
|
+
/** The section's trade name, e.g. "Technical SEO": the gist table's Section column, and the line over the question. */
|
|
46
|
+
name: string;
|
|
47
|
+
/** The question the section answers, in the owner's words, e.g. "Can Google read the site?". The section's heading. */
|
|
48
|
+
question: string;
|
|
49
|
+
/** The baseline's grade. Omit for a section not yet graded; the table shows a quiet dash. */
|
|
50
|
+
grade?: WriteupGrade;
|
|
51
|
+
/** The one line in the gist table. */
|
|
52
|
+
gist: ReadParagraph;
|
|
53
|
+
/** What we found: one paragraph or more. */
|
|
54
|
+
found: ReadParagraph[];
|
|
55
|
+
/** A few concrete examples of what was found, as a short list under it. */
|
|
56
|
+
examples?: ReadParagraph[];
|
|
57
|
+
/** Proof pictures, each over its caption, e.g. a search result capture per market. */
|
|
58
|
+
shots?: WriteupShot[];
|
|
59
|
+
/** Why, in one sentence, set apart. */
|
|
60
|
+
why: ReadParagraph;
|
|
61
|
+
/** What changes: one paragraph or more. */
|
|
62
|
+
changes: ReadParagraph[];
|
|
63
|
+
/** The tools the readings came from, linked, so the owner can open them. */
|
|
64
|
+
sources?: ReadSource[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A tier, e.g. Health: its heading, the question it answers, and its sections in order. */
|
|
68
|
+
export interface ReadTier {
|
|
69
|
+
/** The anchor a header stop points at, e.g. "health". */
|
|
70
|
+
id?: string;
|
|
71
|
+
/** The tier's name, e.g. "Health". */
|
|
72
|
+
title: string;
|
|
73
|
+
/** The question the tier answers, in the owner's words. */
|
|
74
|
+
lead?: string;
|
|
75
|
+
sections: ReadSection[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** One step of the plan: a bold title and a sentence or two. */
|
|
79
|
+
export interface ReadStep {
|
|
80
|
+
title: string;
|
|
81
|
+
body?: ReadParagraph;
|
|
82
|
+
/** Whose step it is when it is not the practice's, e.g. "Yours" or a name; set beside the title. */
|
|
83
|
+
who?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The plan: the steps in order, numbered. */
|
|
87
|
+
export interface ReadPlan {
|
|
88
|
+
/** The anchor a header stop points at, e.g. "plan". */
|
|
89
|
+
id?: string;
|
|
90
|
+
/** The heading; defaults to the `plan` label. */
|
|
91
|
+
title?: string;
|
|
92
|
+
/** A line over the steps. */
|
|
93
|
+
lead?: ReadParagraph;
|
|
94
|
+
steps: ReadStep[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The close: one line, then the sign off. */
|
|
98
|
+
export interface ReadClose {
|
|
99
|
+
/** One sentence, e.g. what follows and when. */
|
|
100
|
+
line: ReadParagraph;
|
|
101
|
+
signOff?: string;
|
|
102
|
+
person?: WriteupPerson;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The fixed words the page sets, each overridable. */
|
|
106
|
+
export interface ReadLabels {
|
|
107
|
+
/** The gist table's first column. */
|
|
108
|
+
section: string;
|
|
109
|
+
/** The gist table's second column. */
|
|
110
|
+
grade: string;
|
|
111
|
+
/** The gist table's third column. */
|
|
112
|
+
gist: string;
|
|
113
|
+
/** What a screen reader hears for a section with no grade. */
|
|
114
|
+
notGraded: string;
|
|
115
|
+
found: string;
|
|
116
|
+
why: string;
|
|
117
|
+
changes: string;
|
|
118
|
+
/** The label over a section's tools. */
|
|
119
|
+
sources: string;
|
|
120
|
+
/** The plan's heading when it names none. */
|
|
121
|
+
plan: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const readLabels: ReadLabels = {
|
|
125
|
+
section: "Section",
|
|
126
|
+
grade: "Grade",
|
|
127
|
+
gist: "The Gist",
|
|
128
|
+
notGraded: "Not graded yet",
|
|
129
|
+
found: "What We Found",
|
|
130
|
+
why: "Why",
|
|
131
|
+
changes: "What Changes",
|
|
132
|
+
sources: "Sources",
|
|
133
|
+
plan: "The Plan",
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export interface ReadProps {
|
|
137
|
+
title: string;
|
|
138
|
+
/** The business, set in the dateline. */
|
|
139
|
+
preparedFor: string;
|
|
140
|
+
/** The ISO date, e.g. "2026-10-16". */
|
|
141
|
+
date: string;
|
|
142
|
+
/** Two or three sentences under the title. */
|
|
143
|
+
lede: ReadParagraph;
|
|
144
|
+
/** The heading over the gist table, e.g. "At a Glance". */
|
|
145
|
+
gistTitle?: string;
|
|
146
|
+
/** The key: what each letter means, in a few words each. */
|
|
147
|
+
gradeKey?: Array<{ grade: WriteupGrade; label: string }>;
|
|
148
|
+
/** The tiers in order. Each section appears in the gist table and in the body. */
|
|
149
|
+
tiers: ReadTier[];
|
|
150
|
+
plan: ReadPlan;
|
|
151
|
+
close: ReadClose;
|
|
152
|
+
labels?: Partial<ReadLabels>;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function Sources({ sources, label }: { sources: ReadSource[]; label: string }) {
|
|
156
|
+
return (
|
|
157
|
+
<footer className="op-sources">
|
|
158
|
+
<span className="op-sources-label">{label}</span>
|
|
159
|
+
<ul>
|
|
160
|
+
{sources.map((source) => (
|
|
161
|
+
<li key={source.label}>
|
|
162
|
+
{source.href ? (
|
|
163
|
+
<a href={source.href} {...newTabProps(source.href, true)}>
|
|
164
|
+
{source.label}
|
|
165
|
+
<ArrowUpRight size={12} aria-hidden="true" />
|
|
166
|
+
</a>
|
|
167
|
+
) : (
|
|
168
|
+
source.label
|
|
169
|
+
)}
|
|
170
|
+
</li>
|
|
171
|
+
))}
|
|
172
|
+
</ul>
|
|
173
|
+
</footer>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function Gist({
|
|
178
|
+
tiers,
|
|
179
|
+
title,
|
|
180
|
+
gradeKey,
|
|
181
|
+
labels,
|
|
182
|
+
}: {
|
|
183
|
+
tiers: ReadTier[];
|
|
184
|
+
title?: string;
|
|
185
|
+
gradeKey?: ReadProps["gradeKey"];
|
|
186
|
+
labels: ReadLabels;
|
|
187
|
+
}) {
|
|
188
|
+
return (
|
|
189
|
+
<section className="rd-gist" aria-label={title ? undefined : labels.gist}>
|
|
190
|
+
{(title || gradeKey) && (
|
|
191
|
+
<div className="op-ov-head">
|
|
192
|
+
{title && <h2 className="op-ov-title">{title}</h2>}
|
|
193
|
+
{gradeKey && (
|
|
194
|
+
<ul className="op-legend">
|
|
195
|
+
{gradeKey.map((entry) => (
|
|
196
|
+
<li key={entry.grade}>
|
|
197
|
+
<GradeChip grade={entry.grade} />
|
|
198
|
+
<span>{entry.label}</span>
|
|
199
|
+
</li>
|
|
200
|
+
))}
|
|
201
|
+
</ul>
|
|
202
|
+
)}
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
<div className="rd-gist-card">
|
|
206
|
+
<table className="rd-gist-table">
|
|
207
|
+
<thead>
|
|
208
|
+
<tr>
|
|
209
|
+
<th scope="col" className="rd-gist-section">
|
|
210
|
+
{labels.section}
|
|
211
|
+
</th>
|
|
212
|
+
<th scope="col" className="rd-gist-grade">
|
|
213
|
+
{labels.grade}
|
|
214
|
+
</th>
|
|
215
|
+
<th scope="col" className="rd-gist-read">
|
|
216
|
+
{labels.gist}
|
|
217
|
+
</th>
|
|
218
|
+
</tr>
|
|
219
|
+
</thead>
|
|
220
|
+
{tiers.map((tier) => (
|
|
221
|
+
<tbody key={tier.title}>
|
|
222
|
+
<tr className="rd-gist-tier">
|
|
223
|
+
<th scope="rowgroup" colSpan={3}>
|
|
224
|
+
{tier.title}
|
|
225
|
+
</th>
|
|
226
|
+
</tr>
|
|
227
|
+
{tier.sections.map((section) => (
|
|
228
|
+
<tr key={section.id}>
|
|
229
|
+
<th scope="row" className="rd-gist-section">
|
|
230
|
+
<a href={`#${section.id}`}>{section.name}</a>
|
|
231
|
+
</th>
|
|
232
|
+
<td className="rd-gist-grade">
|
|
233
|
+
{section.grade ? (
|
|
234
|
+
<>
|
|
235
|
+
<span className="op-sr-only">{gradeLabel[section.grade]}</span>
|
|
236
|
+
<span aria-hidden="true">
|
|
237
|
+
<GradeChip grade={section.grade} />
|
|
238
|
+
</span>
|
|
239
|
+
</>
|
|
240
|
+
) : (
|
|
241
|
+
<>
|
|
242
|
+
<span className="op-sr-only">{labels.notGraded}</span>
|
|
243
|
+
<span className="op-dash" aria-hidden="true" />
|
|
244
|
+
</>
|
|
245
|
+
)}
|
|
246
|
+
</td>
|
|
247
|
+
<td className="rd-gist-read">
|
|
248
|
+
<Runs paragraph={section.gist} />
|
|
249
|
+
</td>
|
|
250
|
+
</tr>
|
|
251
|
+
))}
|
|
252
|
+
</tbody>
|
|
253
|
+
))}
|
|
254
|
+
</table>
|
|
255
|
+
</div>
|
|
256
|
+
</section>
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function Section({ section, labels }: { section: ReadSection; labels: ReadLabels }) {
|
|
261
|
+
const found = section.found.filter((paragraph) => !isEmpty(paragraph));
|
|
262
|
+
const changes = section.changes.filter((paragraph) => !isEmpty(paragraph));
|
|
263
|
+
const shots = section.shots ?? [];
|
|
264
|
+
return (
|
|
265
|
+
<section className="op-topic rd-section" id={section.id}>
|
|
266
|
+
<header className="op-topic-head">
|
|
267
|
+
<div>
|
|
268
|
+
<p className="rd-name">{section.name}</p>
|
|
269
|
+
<h3>{section.question}</h3>
|
|
270
|
+
</div>
|
|
271
|
+
{section.grade && (
|
|
272
|
+
<>
|
|
273
|
+
<span className="op-sr-only">{`${labels.grade} ${gradeLabel[section.grade]}`}</span>
|
|
274
|
+
<span className="rd-grade" aria-hidden="true">
|
|
275
|
+
<GradeChip grade={section.grade} />
|
|
276
|
+
</span>
|
|
277
|
+
</>
|
|
278
|
+
)}
|
|
279
|
+
</header>
|
|
280
|
+
<div className="rd-beat rd-found">
|
|
281
|
+
<h4>{labels.found}</h4>
|
|
282
|
+
{found.map((paragraph, i) => (
|
|
283
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: paragraphs are static content in a fixed order
|
|
284
|
+
<Paragraph paragraph={paragraph} key={i} />
|
|
285
|
+
))}
|
|
286
|
+
{section.examples && section.examples.length > 0 && (
|
|
287
|
+
<ul className="rd-examples">
|
|
288
|
+
{section.examples.map((example, i) => (
|
|
289
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: examples are static content in a fixed order
|
|
290
|
+
<li key={i}>
|
|
291
|
+
<Runs paragraph={example} />
|
|
292
|
+
</li>
|
|
293
|
+
))}
|
|
294
|
+
</ul>
|
|
295
|
+
)}
|
|
296
|
+
{shots.length > 0 && (
|
|
297
|
+
<div className="rd-shots" data-count={Math.min(shots.length, 3)}>
|
|
298
|
+
{shots.map((shot) => (
|
|
299
|
+
<Shot shot={shot} key={shot.src} />
|
|
300
|
+
))}
|
|
301
|
+
</div>
|
|
302
|
+
)}
|
|
303
|
+
</div>
|
|
304
|
+
<div className="rd-beat rd-why">
|
|
305
|
+
<h4>{labels.why}</h4>
|
|
306
|
+
<Paragraph paragraph={section.why} />
|
|
307
|
+
</div>
|
|
308
|
+
<div className="rd-beat rd-changes">
|
|
309
|
+
<h4>{labels.changes}</h4>
|
|
310
|
+
{changes.map((paragraph, i) => (
|
|
311
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: paragraphs are static content in a fixed order
|
|
312
|
+
<Paragraph paragraph={paragraph} key={i} />
|
|
313
|
+
))}
|
|
314
|
+
</div>
|
|
315
|
+
{section.sources && section.sources.length > 0 && (
|
|
316
|
+
<Sources sources={section.sources} label={labels.sources} />
|
|
317
|
+
)}
|
|
318
|
+
</section>
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function Plan({ plan, labels }: { plan: ReadPlan; labels: ReadLabels }) {
|
|
323
|
+
return (
|
|
324
|
+
<section className="rd-plan" id={plan.id}>
|
|
325
|
+
<h2>{plan.title ?? labels.plan}</h2>
|
|
326
|
+
{plan.lead && !isEmpty(plan.lead) && (
|
|
327
|
+
<Paragraph paragraph={plan.lead} className="rd-plan-lead" />
|
|
328
|
+
)}
|
|
329
|
+
<ol className="rd-steps">
|
|
330
|
+
{plan.steps.map((step) => (
|
|
331
|
+
<li className="rd-step" key={step.title}>
|
|
332
|
+
<p>
|
|
333
|
+
<b className="rd-step-title">{step.title}</b>
|
|
334
|
+
{step.who && (
|
|
335
|
+
<>
|
|
336
|
+
{" "}
|
|
337
|
+
<span className="rd-step-who">{step.who}</span>
|
|
338
|
+
</>
|
|
339
|
+
)}
|
|
340
|
+
{step.body && !isEmpty(step.body) && (
|
|
341
|
+
<>
|
|
342
|
+
{" "}
|
|
343
|
+
<Runs paragraph={step.body} />
|
|
344
|
+
</>
|
|
345
|
+
)}
|
|
346
|
+
</p>
|
|
347
|
+
</li>
|
|
348
|
+
))}
|
|
349
|
+
</ol>
|
|
350
|
+
</section>
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function Read({
|
|
355
|
+
title,
|
|
356
|
+
preparedFor,
|
|
357
|
+
date,
|
|
358
|
+
lede,
|
|
359
|
+
gistTitle,
|
|
360
|
+
gradeKey,
|
|
361
|
+
tiers,
|
|
362
|
+
plan,
|
|
363
|
+
close,
|
|
364
|
+
labels: overrides,
|
|
365
|
+
}: ReadProps) {
|
|
366
|
+
const labels = { ...readLabels, ...overrides };
|
|
367
|
+
const shownTiers = tiers.filter((tier) => tier.sections.length > 0);
|
|
368
|
+
return (
|
|
369
|
+
<div className="op rd">
|
|
370
|
+
<article className="op-doc rd-doc">
|
|
371
|
+
<header className="op-head">
|
|
372
|
+
<p className="op-meta">
|
|
373
|
+
Prepared for <b>{preparedFor}</b>
|
|
374
|
+
<span className="op-meta-sep" aria-hidden="true">
|
|
375
|
+
{" · "}
|
|
376
|
+
</span>
|
|
377
|
+
<span className="op-meta-date">{date}</span>
|
|
378
|
+
</p>
|
|
379
|
+
<h1>{title}</h1>
|
|
380
|
+
{!isEmpty(lede) && <Paragraph paragraph={lede} className="op-intro rd-lede" />}
|
|
381
|
+
</header>
|
|
382
|
+
{shownTiers.length > 0 && (
|
|
383
|
+
<Gist tiers={shownTiers} title={gistTitle} gradeKey={gradeKey} labels={labels} />
|
|
384
|
+
)}
|
|
385
|
+
{shownTiers.map((tier) => (
|
|
386
|
+
<div className="rd-tier" key={tier.title}>
|
|
387
|
+
<div className="op-part" id={tier.id}>
|
|
388
|
+
<h2>{tier.title}</h2>
|
|
389
|
+
{tier.lead && <p>{tier.lead}</p>}
|
|
390
|
+
</div>
|
|
391
|
+
{tier.sections.map((section) => (
|
|
392
|
+
<Section section={section} labels={labels} key={section.id} />
|
|
393
|
+
))}
|
|
394
|
+
</div>
|
|
395
|
+
))}
|
|
396
|
+
{plan.steps.length > 0 && <Plan plan={plan} labels={labels} />}
|
|
397
|
+
{!isEmpty(close.line) && <Paragraph paragraph={close.line} className="rd-closing" />}
|
|
398
|
+
{close.signOff && close.person && (
|
|
399
|
+
<footer className="op-close rd-close">
|
|
400
|
+
<SignOff signOff={close.signOff} person={close.person} />
|
|
401
|
+
</footer>
|
|
402
|
+
)}
|
|
403
|
+
</article>
|
|
404
|
+
</div>
|
|
405
|
+
);
|
|
406
|
+
}
|
package/src/artifact/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ export { Gate, type GatePayload, type GatePerson, type GateProps, normalizeEmail
|
|
|
13
13
|
export { GradeChip, gradeLabel } from "./GradeChip";
|
|
14
14
|
export * from "./Note";
|
|
15
15
|
export { buildPageHead, canonicalUrl, type PageMeta } from "./pageHead";
|
|
16
|
+
export * from "./Read";
|
|
16
17
|
export * from "./Report";
|
|
17
18
|
export {
|
|
18
19
|
mintToken,
|
package/src/styles/artifact.css
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
share. Moved whole from the artifact template, in the order its build
|
|
4
4
|
bundled them (the shared classes, the stage, the monthly page, the
|
|
5
5
|
writeup), which the two @page rules depend on: the later one wins, and
|
|
6
|
-
the writeup's is the later one. The note
|
|
7
|
-
no @page, so the writeup's stays the one that wins. They sit in the package's layer like
|
|
6
|
+
the writeup's is the later one. The note and the read follow the writeup
|
|
7
|
+
and set no @page, so the writeup's stays the one that wins. They sit in the package's layer like
|
|
8
8
|
every sheet here, so any rule a site writes outside a layer still wins
|
|
9
9
|
over them whatever its specificity. */
|
|
10
10
|
@layer atelic-ui {
|
|
@@ -2293,4 +2293,289 @@
|
|
|
2293
2293
|
break-inside: avoid;
|
|
2294
2294
|
}
|
|
2295
2295
|
}
|
|
2296
|
+
|
|
2297
|
+
/* =================================================================
|
|
2298
|
+
THE READ: the baseline's grades explained by cause (Read.tsx). The
|
|
2299
|
+
writeup's sibling: it sits inside the writeup's own .op root, so the
|
|
2300
|
+
column, the head, the grade chip and its key, the tier heading, the
|
|
2301
|
+
section card, the screen, the sources, the close, and every print
|
|
2302
|
+
rule of the writeup's are its own already. This block adds only what
|
|
2303
|
+
the read has and the writeup does not: the gist table, the three
|
|
2304
|
+
beats of a section, the proof pictures in a row, and the numbered
|
|
2305
|
+
plan. It sets no @page; the writeup's, earlier in this sheet and the
|
|
2306
|
+
later of the two there, sets letter paper for every artifact page.
|
|
2307
|
+
|
|
2308
|
+
Tokens only (theme.css).
|
|
2309
|
+
================================================================= */
|
|
2310
|
+
|
|
2311
|
+
/* Prose links read as links here, where the writeup's are quiet. */
|
|
2312
|
+
.atelic-ui .rd-lede a,
|
|
2313
|
+
.atelic-ui .rd-beat a,
|
|
2314
|
+
.atelic-ui .rd-gist-read a,
|
|
2315
|
+
.atelic-ui .rd-plan a,
|
|
2316
|
+
.atelic-ui .rd-closing a {
|
|
2317
|
+
color: var(--op-accent);
|
|
2318
|
+
text-decoration: underline;
|
|
2319
|
+
text-underline-offset: 2px;
|
|
2320
|
+
text-decoration-thickness: 1px;
|
|
2321
|
+
overflow-wrap: anywhere;
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
/* ---- the gist table: section, grade, and one line, under its tier ---- */
|
|
2325
|
+
.atelic-ui .rd-gist {
|
|
2326
|
+
margin-top: 26px;
|
|
2327
|
+
}
|
|
2328
|
+
.atelic-ui .rd-gist-card {
|
|
2329
|
+
min-width: 0;
|
|
2330
|
+
background: var(--op-card);
|
|
2331
|
+
border: 1px solid var(--op-rule);
|
|
2332
|
+
border-radius: var(--radius-md);
|
|
2333
|
+
box-shadow: var(--shadow-sm);
|
|
2334
|
+
overflow: hidden;
|
|
2335
|
+
}
|
|
2336
|
+
.atelic-ui .rd-gist-table {
|
|
2337
|
+
width: 100%;
|
|
2338
|
+
border-collapse: collapse;
|
|
2339
|
+
font-size: 15px;
|
|
2340
|
+
line-height: 1.45;
|
|
2341
|
+
}
|
|
2342
|
+
.atelic-ui .rd-gist-table thead th {
|
|
2343
|
+
padding: 9px 12px;
|
|
2344
|
+
text-align: left;
|
|
2345
|
+
font-family: var(--font-mono);
|
|
2346
|
+
font-size: 11px;
|
|
2347
|
+
font-weight: 500;
|
|
2348
|
+
letter-spacing: 0.1em;
|
|
2349
|
+
text-transform: uppercase;
|
|
2350
|
+
color: var(--op-ink-faint);
|
|
2351
|
+
border-bottom: 1px solid var(--op-rule);
|
|
2352
|
+
}
|
|
2353
|
+
.atelic-ui .rd-gist-tier th {
|
|
2354
|
+
padding: 7px 12px;
|
|
2355
|
+
background: var(--surface-alt);
|
|
2356
|
+
text-align: left;
|
|
2357
|
+
font-family: var(--font-mono);
|
|
2358
|
+
font-size: 11.5px;
|
|
2359
|
+
font-weight: 500;
|
|
2360
|
+
letter-spacing: 0.1em;
|
|
2361
|
+
text-transform: uppercase;
|
|
2362
|
+
color: var(--op-ink-soft);
|
|
2363
|
+
}
|
|
2364
|
+
.atelic-ui .rd-gist-table tbody tr + tr > * {
|
|
2365
|
+
border-top: 1px solid var(--op-rule);
|
|
2366
|
+
}
|
|
2367
|
+
.atelic-ui .rd-gist-table tbody th[scope="row"],
|
|
2368
|
+
.atelic-ui .rd-gist-table td {
|
|
2369
|
+
padding: 11px 12px;
|
|
2370
|
+
vertical-align: top;
|
|
2371
|
+
text-align: left;
|
|
2372
|
+
}
|
|
2373
|
+
.atelic-ui .rd-gist-table .rd-gist-section {
|
|
2374
|
+
font-weight: 650;
|
|
2375
|
+
letter-spacing: -0.01em;
|
|
2376
|
+
width: 30%;
|
|
2377
|
+
}
|
|
2378
|
+
.atelic-ui .rd-gist-table tbody .rd-gist-section a:hover {
|
|
2379
|
+
color: var(--op-accent);
|
|
2380
|
+
}
|
|
2381
|
+
.atelic-ui .rd-gist-table .rd-gist-grade {
|
|
2382
|
+
width: 1%;
|
|
2383
|
+
white-space: nowrap;
|
|
2384
|
+
text-align: center;
|
|
2385
|
+
}
|
|
2386
|
+
.atelic-ui .rd-gist-table td.rd-gist-read {
|
|
2387
|
+
color: var(--op-ink-soft);
|
|
2388
|
+
text-wrap: pretty;
|
|
2389
|
+
min-width: 0;
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
/* ---- a section: the trade name over the question, then three beats ---- */
|
|
2393
|
+
.atelic-ui .rd-name {
|
|
2394
|
+
margin: 0 0 4px;
|
|
2395
|
+
font-family: var(--font-mono);
|
|
2396
|
+
font-size: 11.5px;
|
|
2397
|
+
letter-spacing: 0.1em;
|
|
2398
|
+
text-transform: uppercase;
|
|
2399
|
+
color: var(--op-ink-faint);
|
|
2400
|
+
}
|
|
2401
|
+
.atelic-ui .rd-grade .op-grade {
|
|
2402
|
+
width: 42px;
|
|
2403
|
+
height: 42px;
|
|
2404
|
+
border-radius: 12px;
|
|
2405
|
+
font-size: 21px;
|
|
2406
|
+
}
|
|
2407
|
+
.atelic-ui .rd-beat {
|
|
2408
|
+
margin-top: 16px;
|
|
2409
|
+
min-width: 0;
|
|
2410
|
+
}
|
|
2411
|
+
.atelic-ui .rd-beat h4 {
|
|
2412
|
+
margin: 0 0 6px;
|
|
2413
|
+
font-family: var(--font-mono);
|
|
2414
|
+
font-size: 11.5px;
|
|
2415
|
+
font-weight: 500;
|
|
2416
|
+
letter-spacing: 0.1em;
|
|
2417
|
+
text-transform: uppercase;
|
|
2418
|
+
color: var(--op-ink-faint);
|
|
2419
|
+
}
|
|
2420
|
+
.atelic-ui .rd-beat p {
|
|
2421
|
+
margin: 0 0 10px;
|
|
2422
|
+
max-width: 68ch;
|
|
2423
|
+
text-wrap: pretty;
|
|
2424
|
+
}
|
|
2425
|
+
.atelic-ui .rd-beat p:last-child {
|
|
2426
|
+
margin-bottom: 0;
|
|
2427
|
+
}
|
|
2428
|
+
.atelic-ui .rd-examples {
|
|
2429
|
+
list-style: disc;
|
|
2430
|
+
margin: 8px 0 0;
|
|
2431
|
+
padding-left: 1.2em;
|
|
2432
|
+
max-width: 68ch;
|
|
2433
|
+
color: var(--op-ink-soft);
|
|
2434
|
+
}
|
|
2435
|
+
.atelic-ui .rd-examples li + li {
|
|
2436
|
+
margin-top: 4px;
|
|
2437
|
+
}
|
|
2438
|
+
/* Why is one sentence, set apart: a tinted panel with the accent's rule. */
|
|
2439
|
+
.atelic-ui .rd-why {
|
|
2440
|
+
padding: 12px 14px;
|
|
2441
|
+
border-left: 3px solid var(--op-accent);
|
|
2442
|
+
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
|
2443
|
+
background: var(--surface-alt);
|
|
2444
|
+
}
|
|
2445
|
+
.atelic-ui .rd-why p {
|
|
2446
|
+
font-size: 16.5px;
|
|
2447
|
+
font-weight: 550;
|
|
2448
|
+
line-height: 1.45;
|
|
2449
|
+
color: var(--op-ink);
|
|
2450
|
+
}
|
|
2451
|
+
/* The proof pictures sit in a row at one size, a search capture per
|
|
2452
|
+
market; a phone stacks them. */
|
|
2453
|
+
.atelic-ui .rd-shots {
|
|
2454
|
+
display: grid;
|
|
2455
|
+
grid-template-columns: minmax(0, 1fr);
|
|
2456
|
+
gap: 14px;
|
|
2457
|
+
margin-top: 14px;
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
/* ---- the plan: the steps in order, numbered ---- */
|
|
2461
|
+
.atelic-ui .rd-plan {
|
|
2462
|
+
margin-top: 34px;
|
|
2463
|
+
scroll-margin-top: calc(var(--nav-height) + 16px);
|
|
2464
|
+
}
|
|
2465
|
+
.atelic-ui .rd-plan h2 {
|
|
2466
|
+
font-size: 27px;
|
|
2467
|
+
font-weight: 650;
|
|
2468
|
+
letter-spacing: -0.025em;
|
|
2469
|
+
line-height: 1.1;
|
|
2470
|
+
margin: 0;
|
|
2471
|
+
color: var(--op-ink);
|
|
2472
|
+
}
|
|
2473
|
+
.atelic-ui .rd-plan-lead {
|
|
2474
|
+
margin: 6px 0 0;
|
|
2475
|
+
font-size: 16.5px;
|
|
2476
|
+
color: var(--op-ink-soft);
|
|
2477
|
+
}
|
|
2478
|
+
.atelic-ui .rd-steps {
|
|
2479
|
+
list-style: none;
|
|
2480
|
+
counter-reset: rd-step;
|
|
2481
|
+
margin: 12px 0 0;
|
|
2482
|
+
padding: 0;
|
|
2483
|
+
background: var(--op-card);
|
|
2484
|
+
border: 1px solid var(--op-rule);
|
|
2485
|
+
border-radius: var(--radius-md);
|
|
2486
|
+
box-shadow: var(--shadow-sm);
|
|
2487
|
+
}
|
|
2488
|
+
.atelic-ui .rd-step {
|
|
2489
|
+
counter-increment: rd-step;
|
|
2490
|
+
display: grid;
|
|
2491
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
2492
|
+
column-gap: 12px;
|
|
2493
|
+
padding: 14px 16px;
|
|
2494
|
+
}
|
|
2495
|
+
.atelic-ui .rd-step + .rd-step {
|
|
2496
|
+
border-top: 1px solid var(--op-rule);
|
|
2497
|
+
}
|
|
2498
|
+
.atelic-ui .rd-step::before {
|
|
2499
|
+
content: counter(rd-step);
|
|
2500
|
+
display: inline-grid;
|
|
2501
|
+
place-items: center;
|
|
2502
|
+
width: 28px;
|
|
2503
|
+
height: 28px;
|
|
2504
|
+
border: 1.5px solid var(--op-accent);
|
|
2505
|
+
border-radius: 999px;
|
|
2506
|
+
color: var(--op-accent);
|
|
2507
|
+
font-size: 14px;
|
|
2508
|
+
font-weight: 700;
|
|
2509
|
+
line-height: 1;
|
|
2510
|
+
}
|
|
2511
|
+
.atelic-ui .rd-step p {
|
|
2512
|
+
margin: 3px 0 0;
|
|
2513
|
+
color: var(--op-ink-soft);
|
|
2514
|
+
text-wrap: pretty;
|
|
2515
|
+
}
|
|
2516
|
+
.atelic-ui .rd-step-title {
|
|
2517
|
+
font-weight: 650;
|
|
2518
|
+
color: var(--op-ink);
|
|
2519
|
+
}
|
|
2520
|
+
.atelic-ui .rd-step-who {
|
|
2521
|
+
display: inline-block;
|
|
2522
|
+
margin-left: 2px;
|
|
2523
|
+
padding: 1px 8px;
|
|
2524
|
+
border: 1px solid var(--neutral-200);
|
|
2525
|
+
border-radius: 999px;
|
|
2526
|
+
font-family: var(--font-mono);
|
|
2527
|
+
font-size: 11px;
|
|
2528
|
+
letter-spacing: 0.06em;
|
|
2529
|
+
text-transform: uppercase;
|
|
2530
|
+
vertical-align: 0.15em;
|
|
2531
|
+
color: var(--op-ink-soft);
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
/* ---- the close: one line, then the sign off under its rule ---- */
|
|
2535
|
+
.atelic-ui .rd-closing {
|
|
2536
|
+
margin: 34px 0 0;
|
|
2537
|
+
max-width: 62ch;
|
|
2538
|
+
font-size: 17px;
|
|
2539
|
+
line-height: 1.5;
|
|
2540
|
+
color: var(--op-ink);
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
/* ---- wide and on paper: the pictures beside each other ---- */
|
|
2544
|
+
@media print, (min-width: 721px) {
|
|
2545
|
+
.atelic-ui .rd-gist-table tbody th[scope="row"],
|
|
2546
|
+
.atelic-ui .rd-gist-table td,
|
|
2547
|
+
.atelic-ui .rd-gist-table thead th,
|
|
2548
|
+
.atelic-ui .rd-gist-tier th {
|
|
2549
|
+
padding-left: 20px;
|
|
2550
|
+
padding-right: 16px;
|
|
2551
|
+
}
|
|
2552
|
+
.atelic-ui .rd-shots[data-count="2"] {
|
|
2553
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
2554
|
+
}
|
|
2555
|
+
.atelic-ui .rd-shots[data-count="3"] {
|
|
2556
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
/* ---- print: the writeup's rules carry the page; these hold the read's
|
|
2561
|
+
own blocks whole ---- */
|
|
2562
|
+
@media print {
|
|
2563
|
+
.atelic-ui .rd-gist-card,
|
|
2564
|
+
.atelic-ui .rd-steps {
|
|
2565
|
+
box-shadow: none;
|
|
2566
|
+
border-color: var(--neutral-200);
|
|
2567
|
+
}
|
|
2568
|
+
.atelic-ui .rd-gist-table tr,
|
|
2569
|
+
.atelic-ui .rd-closing,
|
|
2570
|
+
.atelic-ui .rd-why,
|
|
2571
|
+
.atelic-ui .rd-step,
|
|
2572
|
+
.atelic-ui .rd-close,
|
|
2573
|
+
.atelic-ui .rd-shots .op-shot {
|
|
2574
|
+
break-inside: avoid;
|
|
2575
|
+
}
|
|
2576
|
+
.atelic-ui .rd-beat h4,
|
|
2577
|
+
.atelic-ui .rd-plan h2 {
|
|
2578
|
+
break-after: avoid;
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2296
2581
|
}
|