@atelic-action/artifact 1.0.0 → 1.2.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 +9 -10
- package/package.json +3 -6
- package/src/documents/Note.tsx +6 -4
- package/src/documents/Read.tsx +230 -92
- package/src/documents/Writeup.tsx +3 -3
- package/src/documents/documents.css +266 -63
- package/src/documents/Paragraph.tsx +0 -65
- package/src/documents/Shot.tsx +0 -38
package/README.md
CHANGED
|
@@ -24,10 +24,10 @@ Every export of the package, all from the one entry, `@atelic-action/artifact`.
|
|
|
24
24
|
| `Writeup`, `WriteupProps` | The graded document an owner gets: the overview, the three tiers of section cards, the close | `/writeup` |
|
|
25
25
|
| `ratingCell` | A star rating table cell for a writeup table, sorted by its figure | `/writeup` |
|
|
26
26
|
| `WriteupBlock`, `WriteupTopic`, `WriteupPart`, `WriteupSheet`, `WriteupSummary`, `WriteupSummaryRow`, `WriteupMarkets`, `WriteupRented`, `WriteupMark`, `WriteupCell`, `WriteupTone`, and the rest of the `Writeup*` types | The shapes a writeup's content is written in | none |
|
|
27
|
-
| `WriteupGrade`, `WriteupPerson`, `WriteupShot` | The grade letter, the sign off person, and the owner's screen, shared by every document
|
|
28
|
-
| `Read`, `ReadProps` | The baseline's grades explained by cause: the gist table
|
|
29
|
-
| `readLabels`, `ReadLabels` | The fixed words the read sets (the gist table's columns,
|
|
30
|
-
| `ReadTier`, `ReadSection`, `ReadStep`, `ReadPlan`, `ReadClose`, `
|
|
27
|
+
| `WriteupGrade`, `WriteupPerson`, `WriteupShot` | The grade letter, the sign off person, and the owner's screen, shared by every document, all three re exported from ui | none |
|
|
28
|
+
| `Read`, `ReadProps` | The baseline's grades explained by cause: the gist table; each section read top to bottom as where we looked (the tools, linked, at the top), what it showed (a short paragraph or two, a compact table of facts, lists of examples under their own headings, and any proof pictures), why (one sentence on a tinted panel), today and after (a two column table), and what we recommend (a checklist); then the plan in order. A beat with no data is left out, heading and all | `/read` |
|
|
29
|
+
| `readLabels`, `ReadLabels` | The fixed words the read sets (Prepared for, the gist table's columns, Where We Looked, What It Showed, Why, Today, After, What We Recommend, the plan's heading), each overridable through `labels` | `/read` |
|
|
30
|
+
| `ReadTier`, `ReadSection`, `ReadTool`, `ReadFact`, `ReadExamples`, `ReadChange`, `ReadStep`, `ReadPlan`, `ReadClose`, `ReadLink`, `ReadParagraph` | The shapes a read's content is written in, all plain data so a routine can write a read as JSON. A section's `tools` were its `sources` in 1.0.0; `sources` is still read, and rendered at the top as tools, and `ReadSource` names the same shape as `ReadTool` | none |
|
|
31
31
|
| `Report`, `ReportProps` | The monthly page: the four questions, the seven grades now and before, the sign off, on one letter sheet | `/report` |
|
|
32
32
|
| `ReportLead`, `ReportLeads`, `ReportGradeRow` | The shapes a monthly page's content is written in | none |
|
|
33
33
|
| `Note`, `NoteProps` | The first touch set as a page: the opener, two lanes of findings, where the work would start, the close | `/notes` |
|
|
@@ -36,7 +36,7 @@ Every export of the package, all from the one entry, `@atelic-action/artifact`.
|
|
|
36
36
|
| `normalizeEmail` | How a typed address becomes the key a payload opens with; the sealing command mirrors it | none |
|
|
37
37
|
| `documents.css` | The documents' styles, at `@atelic-action/artifact/styles/documents.css` | every route |
|
|
38
38
|
|
|
39
|
-
What a document is built from stays in `@atelic-action/ui`, and this package imports it rather than copying it: `GradeChip`, `gradeLabel`, `SignOff`, and `newTabProps` from `@atelic-action/ui/artifact`; `Callout`, `Checklist`, `ColumnChart`, `DataTable`, `Disclosure`, `StackedBar`, `StatRow`, `ThresholdScale`, and their types from `@atelic-action/ui/components`; `Button` from `@atelic-action/ui/primitives`. The shell around a page (`ArtifactShell`), the head (`buildPageHead`), the contract (`ArtifactConfig`), the reader helpers, the error boundary, the access wall, the Vite configuration, and the commands are ui's too, and a site imports them from there.
|
|
39
|
+
What a document is built from stays in `@atelic-action/ui`, and this package imports it rather than copying it: `GradeChip`, `gradeLabel`, `SignOff`, `Shot` (the owner's screen), `Paragraph`, `Runs`, `isEmpty`, `ArtifactAnchor` (the plain data paragraph of the note and the read), and `newTabProps` from `@atelic-action/ui/artifact`; `Callout`, `Checklist`, `ColumnChart`, `DataTable`, `Disclosure`, `StackedBar`, `StatRow`, `ThresholdScale`, and their types from `@atelic-action/ui/components`; `Button` from `@atelic-action/ui/primitives`. The shell around a page (`ArtifactShell`), the head (`buildPageHead`), the contract (`ArtifactConfig`), the reader helpers, the error boundary, the access wall, the Vite configuration, and the commands are ui's too, and a site imports them from there.
|
|
40
40
|
|
|
41
41
|
## Install
|
|
42
42
|
|
|
@@ -44,7 +44,7 @@ What a document is built from stays in `@atelic-action/ui`, and this package imp
|
|
|
44
44
|
bun add @atelic-action/artifact @atelic-action/ui
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
Peer dependencies: `@atelic-action/ui` at `^
|
|
47
|
+
Peer dependencies: `@atelic-action/ui` at `^2.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.
|
|
48
48
|
|
|
49
49
|
```tsx
|
|
50
50
|
import { Writeup } from "@atelic-action/artifact";
|
|
@@ -57,13 +57,13 @@ The documents' sheet goes after every ui sheet and before the site's `theme.css`
|
|
|
57
57
|
|
|
58
58
|
1. The site's fonts (`fonts.css`)
|
|
59
59
|
2. `@atelic-action/ui/styles/base.css`, `chrome.css`, `components.css`, `primitives.css`, and `gate.css`, in that order
|
|
60
|
-
3. `@atelic-action/ui/styles/artifact.css`, the pieces the documents share (the grade chip, the sign off, the shell's logo)
|
|
60
|
+
3. `@atelic-action/ui/styles/artifact.css`, the pieces the documents share (the grade chip, the sign off, the shot, the shell's logo, and the shell's print rules that drop the header, the credit band, and the skip link); it sets no `@page`
|
|
61
61
|
4. `@atelic-action/artifact/styles/documents.css`
|
|
62
62
|
5. The site's `theme.css`, last
|
|
63
63
|
|
|
64
64
|
Every rule in the sheet sits in the `atelic-ui` layer and under the `.atelic-ui` root class, exactly as ui's sheets do, so any rule a site writes outside a layer still wins over it. 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; their order is load bearing.
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
From 1.2.0 the two sheets do not overlap: ui 2.0.0's `artifact.css` holds only the pieces, and this sheet holds only the documents and both `@page` rules. A site on ui 1.x stays on artifact 1.1.x, which carries the shot's rules and the shell's print rules itself.
|
|
67
67
|
|
|
68
68
|
## Tech Stack
|
|
69
69
|
|
|
@@ -89,7 +89,7 @@ No environment variables required. Visit [http://localhost:5173](http://localhos
|
|
|
89
89
|
|
|
90
90
|
## Building a Client Artifact
|
|
91
91
|
|
|
92
|
-
A business's artifacts live in one folder of `atelic-action/writeups`, named by the business's short name and served at `<short>.atelic.me` as its own Vercel project with the folder as its root directory, one path per artifact. That host is the business's one address with the practice: the website build, when one is previewed, sits on it at `/proto`, proxied from its own project ([DEPLOY.md](DEPLOY.md), The Website Build Under /proto). Cut it from this one with `git subtree add --prefix=<short> https://github.com/atelic-action/artifact.git main`, run in `writeups`. A cut is a site, not the package, so it then takes the package from npm instead of carrying its source: delete `src/documents/` and `tst/documents/`, give `package.json` the folder's own name and drop the package's fields (`version`, `files`, `exports`, `sideEffects`, `
|
|
92
|
+
A business's artifacts live in one folder of `atelic-action/writeups`, named by the business's short name and served at `<short>.atelic.me` as its own Vercel project with the folder as its root directory, one path per artifact. That host is the business's one address with the practice: the website build, when one is previewed, sits on it at `/proto`, proxied from its own project ([DEPLOY.md](DEPLOY.md), The Website Build Under /proto). Cut it from this one with `git subtree add --prefix=<short> https://github.com/atelic-action/artifact.git main`, run in `writeups`. A cut is a site, not the package, so it then takes the package from npm instead of carrying its source: delete `src/documents/` and `tst/documents/`, give `package.json` the folder's own name and drop the package's fields (`version`, `files`, `exports`, `sideEffects`, `peerDependencies`), and run `bun add @atelic-action/artifact`. The routes and content already import the documents by the package's name, so nothing else changes. Nothing is pulled forward from here afterward: a document reaches the folder with `bun update @atelic-action/artifact`, and everything else with logic in it is `@atelic-action/ui`'s and arrives with `bun update @atelic-action/ui` ([CLAUDE.md](CLAUDE.md), What a Cut Takes From Here). Delete the artifacts the engagement does not need (the route, the content module, and the prerender entry), then touch four surfaces:
|
|
93
93
|
|
|
94
94
|
1. **Content**, in `src/content/`. One module per artifact: the real read, every number dated and defensible in the room, graded and shaped by the practice's `FOOTPRINT.md` and written to its pitch surface rules (`Brand/voice.md`, Pitch Surfaces 15 to 21). In `writeup.tsx`, `sheets[]` only groups the blocks; print flows the page across as many letter pages as it needs, and `writeupStops` names the header's stops.
|
|
95
95
|
2. **Images**, split by what they are. Logos are brand marks and go in `public/brand/` (the recipient's logo, e.g. `public/brand/<name>.svg`), which the access wall leaves open so the header and the login page can draw them. The owner's own screens (the full screenshot for the click through and a tight crop for the column) and the QR for the twin, pointing at the bare host (`qr "https://<short>.atelic.me" > public/images/qr.png` with the `qrcode` package), go in `public/images/`, which the wall holds on a private artifact. A cut made before this split moves its recipient logo from `public/images/` to `public/brand/` and repoints `recipient.logo.src`.
|
|
@@ -149,7 +149,6 @@ A document is fixed or grown here, in `src/documents/`, with its test in `tst/do
|
|
|
149
149
|
|
|
150
150
|
Two things differ from ui. A Trusted Publisher is set on a package that already exists, so the first version, 1.0.0, is published by hand by an owner of the `@atelic-action` scope (`npm publish --access public` from a clean checkout of the tag) before the Trusted Publisher is added and the workflow takes over. And this repo is private, so npm attaches no provenance to what it publishes; the package's source is public on npm all the same, which is why `files` ships only `src/documents/` (README.md and package.json ride along, as npm always includes them), and why nothing in them may name a client.
|
|
151
151
|
|
|
152
|
-
|
|
153
152
|
## License
|
|
154
153
|
|
|
155
154
|
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atelic-action/artifact",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "The composed documents the Atelic practice hands or sends a business (the writeup, the read, the monthly page, the note, and the sealed document), built from the pieces in @atelic-action/ui",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -8,9 +8,6 @@
|
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "git+https://github.com/atelic-action/artifact.git"
|
|
10
10
|
},
|
|
11
|
-
"publishConfig": {
|
|
12
|
-
"access": "public"
|
|
13
|
-
},
|
|
14
11
|
"files": [
|
|
15
12
|
"src/documents"
|
|
16
13
|
],
|
|
@@ -40,12 +37,12 @@
|
|
|
40
37
|
"format": "biome format . --write"
|
|
41
38
|
},
|
|
42
39
|
"peerDependencies": {
|
|
43
|
-
"@atelic-action/ui": "^
|
|
40
|
+
"@atelic-action/ui": "^2.0.0",
|
|
44
41
|
"lucide-react": "^1.23.0",
|
|
45
42
|
"react": "^19.0.0"
|
|
46
43
|
},
|
|
47
44
|
"devDependencies": {
|
|
48
|
-
"@atelic-action/ui": "^
|
|
45
|
+
"@atelic-action/ui": "^2.0.0",
|
|
49
46
|
"@biomejs/biome": "^2.0.0",
|
|
50
47
|
"@playwright/test": "^1.60.0",
|
|
51
48
|
"@sentry/react": "^10.0.0",
|
package/src/documents/Note.tsx
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { SignOff, type WriteupPerson } from "@atelic-action/ui/artifact";
|
|
2
|
-
import { Callout } from "@atelic-action/ui/components";
|
|
3
1
|
import {
|
|
4
2
|
type ArtifactLink,
|
|
5
3
|
type ArtifactParagraph,
|
|
6
4
|
isEmpty,
|
|
7
5
|
ArtifactAnchor as NoteAnchor,
|
|
8
6
|
Paragraph,
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
Shot,
|
|
8
|
+
SignOff,
|
|
9
|
+
type WriteupPerson,
|
|
10
|
+
type WriteupShot,
|
|
11
|
+
} from "@atelic-action/ui/artifact";
|
|
12
|
+
import { Callout } from "@atelic-action/ui/components";
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* The note: the first touch, for a business whose site publishes no email
|
package/src/documents/Read.tsx
CHANGED
|
@@ -1,34 +1,45 @@
|
|
|
1
1
|
import {
|
|
2
|
+
type ArtifactLink,
|
|
3
|
+
type ArtifactParagraph,
|
|
2
4
|
GradeChip,
|
|
3
5
|
gradeLabel,
|
|
6
|
+
isEmpty,
|
|
4
7
|
newTabProps,
|
|
8
|
+
Paragraph,
|
|
9
|
+
Runs,
|
|
10
|
+
Shot,
|
|
5
11
|
SignOff,
|
|
6
12
|
type WriteupGrade,
|
|
7
13
|
type WriteupPerson,
|
|
14
|
+
type WriteupShot,
|
|
8
15
|
} from "@atelic-action/ui/artifact";
|
|
9
|
-
import { ArrowUpRight } from "lucide-react";
|
|
10
|
-
import { type ArtifactLink, type ArtifactParagraph, isEmpty, Paragraph, Runs } from "./Paragraph";
|
|
11
|
-
import { Shot, type WriteupShot } from "./Shot";
|
|
16
|
+
import { ArrowUpRight, Check } from "lucide-react";
|
|
12
17
|
|
|
13
18
|
/**
|
|
14
19
|
* The read: what a customer gets at the end of an engagement's first week,
|
|
15
20
|
* before anything changes. Where the writeup says what letter each section
|
|
16
21
|
* earned, the read says why, and what changes. Its shape is the practice's
|
|
17
22
|
* FOOTPRINT.md, under What the Owner Gets: a title and a dated lede; the gist
|
|
18
|
-
* table (every section, its grade, and one line); then the tiers
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
23
|
+
* table (every section, its grade, and one line); then the tiers; then the
|
|
24
|
+
* plan, in order; then one closing line and the sign off.
|
|
25
|
+
*
|
|
26
|
+
* Each section reads as an aggregator of the tools we looked through, top to
|
|
27
|
+
* bottom: where we looked (the tools, linked, each with an optional line);
|
|
28
|
+
* what it showed (a short paragraph or two, a compact table of facts, lists
|
|
29
|
+
* of examples, and any proof pictures); why, in one sentence on a tinted
|
|
30
|
+
* panel; today and after, a two column table of what a thing is and what it
|
|
31
|
+
* becomes; and what we recommend, as a checklist. A beat with nothing in it
|
|
32
|
+
* is left out, heading and all.
|
|
22
33
|
*
|
|
23
34
|
* It is the writeup's sibling, not a variant: it wears the writeup's column,
|
|
24
|
-
* head, grade chip and key, tier heading, section card, screen,
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
35
|
+
* head, grade chip and key, tier heading, section card, screen, and close
|
|
36
|
+
* (the `.op` classes), and adds only what is its own (the `.rd` classes).
|
|
37
|
+
* The gist table is drawn from the tiers, so a tier added later (Conversion,
|
|
38
|
+
* once the analytics access lands) is one more entry in `tiers` and appears
|
|
39
|
+
* in the table and the body alike, with no change to the props. Every prop
|
|
40
|
+
* is plain data, so a routine can write a read as JSON; the one exception is
|
|
41
|
+
* the sign off person's `role`, which takes a node and is equally happy with
|
|
42
|
+
* a string.
|
|
32
43
|
*/
|
|
33
44
|
|
|
34
45
|
/** A link inside running text: the words the reader clicks and where they go. */
|
|
@@ -37,13 +48,36 @@ export type ReadLink = ArtifactLink;
|
|
|
37
48
|
/** A paragraph: a plain string, or runs of text and links in order. */
|
|
38
49
|
export type ReadParagraph = ArtifactParagraph;
|
|
39
50
|
|
|
40
|
-
/** A
|
|
41
|
-
export interface
|
|
51
|
+
/** A tool we looked through: its name, where it opens (in a new tab), and a line on what we read there. */
|
|
52
|
+
export interface ReadTool {
|
|
42
53
|
label: string;
|
|
43
54
|
href?: string;
|
|
55
|
+
/** One short line, e.g. "Page indexing, the last 90 days". */
|
|
56
|
+
note?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The 1.0.0 name for a tool; the same shape. */
|
|
60
|
+
export type ReadSource = ReadTool;
|
|
61
|
+
|
|
62
|
+
/** One row of a section's facts: a short label and its value, e.g. "Pages on Google" and "9 of 9". */
|
|
63
|
+
export interface ReadFact {
|
|
64
|
+
label: string;
|
|
65
|
+
value: ReadParagraph;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A list of examples under its own short heading; each item a paragraph that can carry links. */
|
|
69
|
+
export interface ReadExamples {
|
|
70
|
+
title?: string;
|
|
71
|
+
items: ReadParagraph[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** One row of today and after: what a thing is now, and what it becomes. */
|
|
75
|
+
export interface ReadChange {
|
|
76
|
+
now: ReadParagraph;
|
|
77
|
+
after: ReadParagraph;
|
|
44
78
|
}
|
|
45
79
|
|
|
46
|
-
/** One section of a tier: a question in
|
|
80
|
+
/** One section of a tier: a question, read top to bottom in five beats. */
|
|
47
81
|
export interface ReadSection {
|
|
48
82
|
/** The anchor the gist table links to, e.g. "technical-seo". */
|
|
49
83
|
id: string;
|
|
@@ -55,18 +89,24 @@ export interface ReadSection {
|
|
|
55
89
|
grade?: WriteupGrade;
|
|
56
90
|
/** The one line in the gist table. */
|
|
57
91
|
gist: ReadParagraph;
|
|
58
|
-
/**
|
|
92
|
+
/** Where we looked: the tools the readings came from, linked, at the top of the section. */
|
|
93
|
+
tools?: ReadTool[];
|
|
94
|
+
/** The 1.0.0 name for `tools`, still read and rendered in the same place; `tools` wins when both are set. */
|
|
95
|
+
sources?: ReadTool[];
|
|
96
|
+
/** What it showed: one or two short paragraphs. */
|
|
59
97
|
found: ReadParagraph[];
|
|
60
|
-
/**
|
|
61
|
-
|
|
98
|
+
/** What it showed, as a compact two column table of facts under the paragraphs. */
|
|
99
|
+
facts?: ReadFact[];
|
|
100
|
+
/** What it showed, as lists of examples, each under its own short heading. */
|
|
101
|
+
examples?: ReadExamples[];
|
|
62
102
|
/** Proof pictures, each over its caption, e.g. a search result capture per market. */
|
|
63
103
|
shots?: WriteupShot[];
|
|
64
104
|
/** Why, in one sentence, set apart. */
|
|
65
105
|
why: ReadParagraph;
|
|
66
|
-
/**
|
|
67
|
-
changes
|
|
68
|
-
/**
|
|
69
|
-
|
|
106
|
+
/** Today and after: what each thing is, and what it becomes. */
|
|
107
|
+
changes?: ReadChange[];
|
|
108
|
+
/** What we recommend, as a checklist. */
|
|
109
|
+
recommend?: ReadParagraph[];
|
|
70
110
|
}
|
|
71
111
|
|
|
72
112
|
/** A tier, e.g. Health: its heading, the question it answers, and its sections in order. */
|
|
@@ -109,6 +149,8 @@ export interface ReadClose {
|
|
|
109
149
|
|
|
110
150
|
/** The fixed words the page sets, each overridable. */
|
|
111
151
|
export interface ReadLabels {
|
|
152
|
+
/** The dateline's words before the business, e.g. "Prepared for". */
|
|
153
|
+
preparedFor: string;
|
|
112
154
|
/** The gist table's first column. */
|
|
113
155
|
section: string;
|
|
114
156
|
/** The gist table's second column. */
|
|
@@ -117,24 +159,34 @@ export interface ReadLabels {
|
|
|
117
159
|
gist: string;
|
|
118
160
|
/** What a screen reader hears for a section with no grade. */
|
|
119
161
|
notGraded: string;
|
|
162
|
+
/** The beat at the top of a section: the tools we looked through. */
|
|
163
|
+
tools: string;
|
|
164
|
+
/** The beat that says what the tools showed. */
|
|
120
165
|
found: string;
|
|
166
|
+
/** The one sentence beat. */
|
|
121
167
|
why: string;
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
168
|
+
/** The today and after table's first column. */
|
|
169
|
+
now: string;
|
|
170
|
+
/** The today and after table's second column. */
|
|
171
|
+
after: string;
|
|
172
|
+
/** The checklist beat at the foot of a section. */
|
|
173
|
+
recommend: string;
|
|
125
174
|
/** The plan's heading when it names none. */
|
|
126
175
|
plan: string;
|
|
127
176
|
}
|
|
128
177
|
|
|
129
178
|
export const readLabels: ReadLabels = {
|
|
179
|
+
preparedFor: "Prepared for",
|
|
130
180
|
section: "Section",
|
|
131
181
|
grade: "Grade",
|
|
132
182
|
gist: "The Gist",
|
|
133
183
|
notGraded: "Not graded yet",
|
|
134
|
-
|
|
184
|
+
tools: "Where We Looked",
|
|
185
|
+
found: "What It Showed",
|
|
135
186
|
why: "Why",
|
|
136
|
-
|
|
137
|
-
|
|
187
|
+
now: "Today",
|
|
188
|
+
after: "After",
|
|
189
|
+
recommend: "What We Recommend",
|
|
138
190
|
plan: "The Plan",
|
|
139
191
|
};
|
|
140
192
|
|
|
@@ -157,25 +209,101 @@ export interface ReadProps {
|
|
|
157
209
|
labels?: Partial<ReadLabels>;
|
|
158
210
|
}
|
|
159
211
|
|
|
160
|
-
|
|
212
|
+
/** A paragraph with something to read. Content may arrive as JSON, so a missing one counts as empty. */
|
|
213
|
+
function filled(paragraph: ReadParagraph | undefined | null): paragraph is ReadParagraph {
|
|
214
|
+
return paragraph !== undefined && paragraph !== null && !isEmpty(paragraph);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function Tools({ tools, label }: { tools: ReadTool[]; label: string }) {
|
|
161
218
|
return (
|
|
162
|
-
<
|
|
163
|
-
<
|
|
164
|
-
<ul>
|
|
165
|
-
{
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
219
|
+
<div className="rd-beat rd-tools">
|
|
220
|
+
<h4>{label}</h4>
|
|
221
|
+
<ul className="rd-tools-list">
|
|
222
|
+
{tools.map((tool, i) => (
|
|
223
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: tools are static content in a fixed order, and a label may repeat
|
|
224
|
+
<li key={i}>
|
|
225
|
+
{tool.href ? (
|
|
226
|
+
<a href={tool.href} {...newTabProps(tool.href, true)}>
|
|
227
|
+
{tool.label}
|
|
228
|
+
<ArrowUpRight size={13} aria-hidden="true" />
|
|
171
229
|
</a>
|
|
172
230
|
) : (
|
|
173
|
-
|
|
231
|
+
<span className="rd-tool-name">{tool.label}</span>
|
|
174
232
|
)}
|
|
233
|
+
{tool.note && <span className="rd-tool-note">{tool.note}</span>}
|
|
234
|
+
</li>
|
|
235
|
+
))}
|
|
236
|
+
</ul>
|
|
237
|
+
</div>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function Facts({ facts }: { facts: ReadFact[] }) {
|
|
242
|
+
return (
|
|
243
|
+
<table className="rd-facts">
|
|
244
|
+
<tbody>
|
|
245
|
+
{facts.map((fact, i) => (
|
|
246
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: facts are static content in a fixed order
|
|
247
|
+
<tr key={i}>
|
|
248
|
+
<th scope="row">{fact.label}</th>
|
|
249
|
+
<td>
|
|
250
|
+
<Runs paragraph={fact.value} />
|
|
251
|
+
</td>
|
|
252
|
+
</tr>
|
|
253
|
+
))}
|
|
254
|
+
</tbody>
|
|
255
|
+
</table>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function Changes({ changes, labels }: { changes: ReadChange[]; labels: ReadLabels }) {
|
|
260
|
+
return (
|
|
261
|
+
<div className="rd-beat rd-changes">
|
|
262
|
+
<table className="rd-changes-table">
|
|
263
|
+
<thead>
|
|
264
|
+
<tr>
|
|
265
|
+
<th scope="col" className="rd-now">
|
|
266
|
+
{labels.now}
|
|
267
|
+
</th>
|
|
268
|
+
<th scope="col" className="rd-after">
|
|
269
|
+
{labels.after}
|
|
270
|
+
</th>
|
|
271
|
+
</tr>
|
|
272
|
+
</thead>
|
|
273
|
+
<tbody>
|
|
274
|
+
{changes.map((change, i) => (
|
|
275
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: rows are static content in a fixed order
|
|
276
|
+
<tr key={i}>
|
|
277
|
+
<td className="rd-now" data-label={labels.now}>
|
|
278
|
+
{filled(change.now) && <Runs paragraph={change.now} />}
|
|
279
|
+
</td>
|
|
280
|
+
<td className="rd-after" data-label={labels.after}>
|
|
281
|
+
{filled(change.after) && <Runs paragraph={change.after} />}
|
|
282
|
+
</td>
|
|
283
|
+
</tr>
|
|
284
|
+
))}
|
|
285
|
+
</tbody>
|
|
286
|
+
</table>
|
|
287
|
+
</div>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function Recommend({ items, label }: { items: ReadParagraph[]; label: string }) {
|
|
292
|
+
return (
|
|
293
|
+
<div className="rd-beat rd-recommend">
|
|
294
|
+
<h4>{label}</h4>
|
|
295
|
+
<ul className="rd-checks">
|
|
296
|
+
{items.map((item, i) => (
|
|
297
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: recommendations are static content in a fixed order
|
|
298
|
+
<li key={i}>
|
|
299
|
+
<Check size={16} strokeWidth={2.5} aria-hidden="true" />
|
|
300
|
+
<span>
|
|
301
|
+
<Runs paragraph={item} />
|
|
302
|
+
</span>
|
|
175
303
|
</li>
|
|
176
304
|
))}
|
|
177
305
|
</ul>
|
|
178
|
-
</
|
|
306
|
+
</div>
|
|
179
307
|
);
|
|
180
308
|
}
|
|
181
309
|
|
|
@@ -190,14 +318,15 @@ function Gist({
|
|
|
190
318
|
gradeKey?: ReadProps["gradeKey"];
|
|
191
319
|
labels: ReadLabels;
|
|
192
320
|
}) {
|
|
321
|
+
const key = gradeKey ?? [];
|
|
193
322
|
return (
|
|
194
323
|
<section className="rd-gist" aria-label={title ? undefined : labels.gist}>
|
|
195
|
-
{(title ||
|
|
324
|
+
{(title || key.length > 0) && (
|
|
196
325
|
<div className="op-ov-head">
|
|
197
326
|
{title && <h2 className="op-ov-title">{title}</h2>}
|
|
198
|
-
{
|
|
327
|
+
{key.length > 0 && (
|
|
199
328
|
<ul className="op-legend">
|
|
200
|
-
{
|
|
329
|
+
{key.map((entry) => (
|
|
201
330
|
<li key={entry.grade}>
|
|
202
331
|
<GradeChip grade={entry.grade} />
|
|
203
332
|
<span>{entry.label}</span>
|
|
@@ -263,9 +392,18 @@ function Gist({
|
|
|
263
392
|
}
|
|
264
393
|
|
|
265
394
|
function Section({ section, labels }: { section: ReadSection; labels: ReadLabels }) {
|
|
266
|
-
const
|
|
267
|
-
const
|
|
395
|
+
const tools = (section.tools ?? section.sources ?? []).filter((tool) => tool?.label?.trim());
|
|
396
|
+
const found = (section.found ?? []).filter(filled);
|
|
397
|
+
const facts = (section.facts ?? []).filter((fact) => filled(fact?.value));
|
|
398
|
+
const examples = (section.examples ?? [])
|
|
399
|
+
.map((list) => ({ title: list?.title, items: (list?.items ?? []).filter(filled) }))
|
|
400
|
+
.filter((list) => list.items.length > 0);
|
|
268
401
|
const shots = section.shots ?? [];
|
|
402
|
+
const changes = (section.changes ?? []).filter(
|
|
403
|
+
(change) => filled(change?.now) || filled(change?.after),
|
|
404
|
+
);
|
|
405
|
+
const recommend = (section.recommend ?? []).filter(filled);
|
|
406
|
+
const showed = found.length + facts.length + examples.length + shots.length > 0;
|
|
269
407
|
return (
|
|
270
408
|
<section className="op-topic rd-section" id={section.id}>
|
|
271
409
|
<header className="op-topic-head">
|
|
@@ -282,44 +420,46 @@ function Section({ section, labels }: { section: ReadSection; labels: ReadLabels
|
|
|
282
420
|
</>
|
|
283
421
|
)}
|
|
284
422
|
</header>
|
|
285
|
-
<
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
</
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
// biome-ignore lint/suspicious/noArrayIndexKey: paragraphs are static content in a fixed order
|
|
317
|
-
<Paragraph paragraph={paragraph} key={i} />
|
|
318
|
-
))}
|
|
319
|
-
</div>
|
|
320
|
-
{section.sources && section.sources.length > 0 && (
|
|
321
|
-
<Sources sources={section.sources} label={labels.sources} />
|
|
423
|
+
{tools.length > 0 && <Tools tools={tools} label={labels.tools} />}
|
|
424
|
+
{showed && (
|
|
425
|
+
<div className="rd-beat rd-found">
|
|
426
|
+
<h4>{labels.found}</h4>
|
|
427
|
+
{found.map((paragraph, i) => (
|
|
428
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: paragraphs are static content in a fixed order
|
|
429
|
+
<Paragraph paragraph={paragraph} key={i} />
|
|
430
|
+
))}
|
|
431
|
+
{facts.length > 0 && <Facts facts={facts} />}
|
|
432
|
+
{examples.map((list, i) => (
|
|
433
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: lists are static content in a fixed order, and a title may repeat
|
|
434
|
+
<div className="rd-examples-block" key={i}>
|
|
435
|
+
{list.title && <h5>{list.title}</h5>}
|
|
436
|
+
<ul className="rd-examples">
|
|
437
|
+
{list.items.map((example, j) => (
|
|
438
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: examples are static content in a fixed order
|
|
439
|
+
<li key={j}>
|
|
440
|
+
<Runs paragraph={example} />
|
|
441
|
+
</li>
|
|
442
|
+
))}
|
|
443
|
+
</ul>
|
|
444
|
+
</div>
|
|
445
|
+
))}
|
|
446
|
+
{shots.length > 0 && (
|
|
447
|
+
<div className="rd-shots" data-count={Math.min(shots.length, 3)}>
|
|
448
|
+
{shots.map((shot) => (
|
|
449
|
+
<Shot shot={shot} key={shot.src} />
|
|
450
|
+
))}
|
|
451
|
+
</div>
|
|
452
|
+
)}
|
|
453
|
+
</div>
|
|
322
454
|
)}
|
|
455
|
+
{filled(section.why) && (
|
|
456
|
+
<div className="rd-beat rd-why">
|
|
457
|
+
<h4>{labels.why}</h4>
|
|
458
|
+
<Paragraph paragraph={section.why} />
|
|
459
|
+
</div>
|
|
460
|
+
)}
|
|
461
|
+
{changes.length > 0 && <Changes changes={changes} labels={labels} />}
|
|
462
|
+
{recommend.length > 0 && <Recommend items={recommend} label={labels.recommend} />}
|
|
323
463
|
</section>
|
|
324
464
|
);
|
|
325
465
|
}
|
|
@@ -328,9 +468,7 @@ function Plan({ plan, labels }: { plan: ReadPlan; labels: ReadLabels }) {
|
|
|
328
468
|
return (
|
|
329
469
|
<section className="rd-plan" id={plan.id}>
|
|
330
470
|
<h2>{plan.title ?? labels.plan}</h2>
|
|
331
|
-
{plan.lead &&
|
|
332
|
-
<Paragraph paragraph={plan.lead} className="rd-plan-lead" />
|
|
333
|
-
)}
|
|
471
|
+
{filled(plan.lead) && <Paragraph paragraph={plan.lead} className="rd-plan-lead" />}
|
|
334
472
|
<ol className="rd-steps">
|
|
335
473
|
{plan.steps.map((step) => (
|
|
336
474
|
<li className="rd-step" key={step.title}>
|
|
@@ -342,7 +480,7 @@ function Plan({ plan, labels }: { plan: ReadPlan; labels: ReadLabels }) {
|
|
|
342
480
|
<span className="rd-step-who">{step.who}</span>
|
|
343
481
|
</>
|
|
344
482
|
)}
|
|
345
|
-
{
|
|
483
|
+
{filled(step.body) && (
|
|
346
484
|
<>
|
|
347
485
|
{" "}
|
|
348
486
|
<Runs paragraph={step.body} />
|
|
@@ -375,14 +513,14 @@ export function Read({
|
|
|
375
513
|
<article className="op-doc rd-doc">
|
|
376
514
|
<header className="op-head">
|
|
377
515
|
<p className="op-meta">
|
|
378
|
-
|
|
516
|
+
{labels.preparedFor} <b>{preparedFor}</b>
|
|
379
517
|
<span className="op-meta-sep" aria-hidden="true">
|
|
380
518
|
{" · "}
|
|
381
519
|
</span>
|
|
382
520
|
<span className="op-meta-date">{date}</span>
|
|
383
521
|
</p>
|
|
384
522
|
<h1>{title}</h1>
|
|
385
|
-
{
|
|
523
|
+
{filled(lede) && <Paragraph paragraph={lede} className="op-intro rd-lede" />}
|
|
386
524
|
</header>
|
|
387
525
|
{shownTiers.length > 0 && (
|
|
388
526
|
<Gist tiers={shownTiers} title={gistTitle} gradeKey={gradeKey} labels={labels} />
|
|
@@ -399,7 +537,7 @@ export function Read({
|
|
|
399
537
|
</div>
|
|
400
538
|
))}
|
|
401
539
|
{plan.steps.length > 0 && <Plan plan={plan} labels={labels} />}
|
|
402
|
-
{
|
|
540
|
+
{filled(close.line) && <Paragraph paragraph={close.line} className="rd-closing" />}
|
|
403
541
|
{close.signOff && close.person && (
|
|
404
542
|
<footer className="op-close rd-close">
|
|
405
543
|
<SignOff signOff={close.signOff} person={close.person} />
|
|
@@ -2,9 +2,11 @@ import {
|
|
|
2
2
|
GradeChip,
|
|
3
3
|
gradeLabel,
|
|
4
4
|
newTabProps,
|
|
5
|
+
Shot,
|
|
5
6
|
SignOff,
|
|
6
7
|
type WriteupGrade,
|
|
7
8
|
type WriteupPerson,
|
|
9
|
+
type WriteupShot,
|
|
8
10
|
} from "@atelic-action/ui/artifact";
|
|
9
11
|
import {
|
|
10
12
|
Callout,
|
|
@@ -25,10 +27,8 @@ import {
|
|
|
25
27
|
} from "@atelic-action/ui/components";
|
|
26
28
|
import { ArrowUpRight, ChevronRight, CircleAlert, CircleCheck, CircleX, Star } from "lucide-react";
|
|
27
29
|
import type { ReactNode } from "react";
|
|
28
|
-
import { Shot, type WriteupShot } from "./Shot";
|
|
29
30
|
|
|
30
|
-
export type { WriteupGrade, WriteupPerson } from "@atelic-action/ui/artifact";
|
|
31
|
-
export type { WriteupShot } from "./Shot";
|
|
31
|
+
export type { WriteupGrade, WriteupPerson, WriteupShot } from "@atelic-action/ui/artifact";
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* The writeup: the graded document an owner gets. It is a web page first: an
|
|
@@ -7,11 +7,14 @@
|
|
|
7
7
|
writeup's stays the one that wins.
|
|
8
8
|
|
|
9
9
|
The pieces the documents share stay in @atelic-action/ui: the grade chip
|
|
10
|
-
(.op-grade), the sign off (.op-who, .op-sign),
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
(.op-grade), the sign off (.op-who, .op-sign), the shot (.op-shot), the
|
|
11
|
+
shell's logo rule, and the shell's print rules that drop the header, the
|
|
12
|
+
credit band, and the skip link on every artifact page, in its
|
|
13
|
+
styles/artifact.css, which sets no @page; the login screen's vocabulary
|
|
14
|
+
the sealed stage reuses, in its styles/gate.css; the page components (the
|
|
15
|
+
callout, the checklist, the table, the charts), in its
|
|
16
|
+
styles/components.css. Import this sheet after all of them and before the
|
|
17
|
+
site's theme.css. Both @page rules live only here.
|
|
15
18
|
|
|
16
19
|
Every rule sits in the same atelic-ui layer as the package sheets, so any
|
|
17
20
|
rule a site writes outside a layer still wins over it whatever its
|
|
@@ -521,7 +524,8 @@
|
|
|
521
524
|
never reflowed, so a print of one sheet is the check.
|
|
522
525
|
|
|
523
526
|
Tokens only (theme.css), except the printed sheet's white. The grade
|
|
524
|
-
chip
|
|
527
|
+
chip, the sign off, and the print rule that drops the header and the
|
|
528
|
+
credit band are the shared classes in @atelic-action/ui's
|
|
525
529
|
styles/artifact.css.
|
|
526
530
|
================================================================= */
|
|
527
531
|
.atelic-ui .rp {
|
|
@@ -797,11 +801,6 @@
|
|
|
797
801
|
padding: 0;
|
|
798
802
|
background: none;
|
|
799
803
|
}
|
|
800
|
-
.atelic-ui:has(.rp) .nav,
|
|
801
|
-
.atelic-ui:has(.rp) .skip-link,
|
|
802
|
-
.atelic-ui:has(.rp) .credit-bar {
|
|
803
|
-
display: none;
|
|
804
|
-
}
|
|
805
804
|
.atelic-ui .rp {
|
|
806
805
|
padding: 0;
|
|
807
806
|
background: none;
|
|
@@ -1345,33 +1344,6 @@
|
|
|
1345
1344
|
gap: 14px;
|
|
1346
1345
|
max-width: 340px;
|
|
1347
1346
|
}
|
|
1348
|
-
.atelic-ui .op-shot {
|
|
1349
|
-
display: flex;
|
|
1350
|
-
flex-direction: column;
|
|
1351
|
-
gap: 6px;
|
|
1352
|
-
margin: 0;
|
|
1353
|
-
}
|
|
1354
|
-
.atelic-ui .op-shot a,
|
|
1355
|
-
.atelic-ui .op-shot-frame {
|
|
1356
|
-
display: block;
|
|
1357
|
-
width: 100%;
|
|
1358
|
-
border-radius: var(--radius-sm);
|
|
1359
|
-
overflow: hidden;
|
|
1360
|
-
border: 1px solid var(--neutral-200);
|
|
1361
|
-
background: var(--op-card);
|
|
1362
|
-
}
|
|
1363
|
-
.atelic-ui .op-shot a {
|
|
1364
|
-
cursor: zoom-in;
|
|
1365
|
-
}
|
|
1366
|
-
/* The expand badge on a linked crop: off here, the note turns it on. */
|
|
1367
|
-
.atelic-ui .op-shot-open {
|
|
1368
|
-
display: none;
|
|
1369
|
-
}
|
|
1370
|
-
.atelic-ui .op-shot img {
|
|
1371
|
-
width: 100%;
|
|
1372
|
-
height: auto;
|
|
1373
|
-
display: block;
|
|
1374
|
-
}
|
|
1375
1347
|
.atelic-ui .op-shot-brief {
|
|
1376
1348
|
display: grid;
|
|
1377
1349
|
place-items: center;
|
|
@@ -1384,11 +1356,6 @@
|
|
|
1384
1356
|
padding: 12px;
|
|
1385
1357
|
text-align: center;
|
|
1386
1358
|
}
|
|
1387
|
-
.atelic-ui .op-shot figcaption {
|
|
1388
|
-
font-size: 12.5px;
|
|
1389
|
-
line-height: 1.45;
|
|
1390
|
-
color: var(--op-ink-faint);
|
|
1391
|
-
}
|
|
1392
1359
|
|
|
1393
1360
|
/* sources: the tools the readings came from */
|
|
1394
1361
|
.atelic-ui .op-sources {
|
|
@@ -1715,9 +1682,6 @@
|
|
|
1715
1682
|
html:has(.atelic-ui .op-doc) {
|
|
1716
1683
|
zoom: 0.74;
|
|
1717
1684
|
}
|
|
1718
|
-
.atelic-ui .nav,
|
|
1719
|
-
.atelic-ui .credit-bar,
|
|
1720
|
-
.atelic-ui .skip-link,
|
|
1721
1685
|
.atelic-ui .op-ov-go,
|
|
1722
1686
|
.atelic-ui .op-topic h3 a svg {
|
|
1723
1687
|
display: none;
|
|
@@ -1808,9 +1772,6 @@
|
|
|
1808
1772
|
.atelic-ui .data-table thead {
|
|
1809
1773
|
break-after: avoid;
|
|
1810
1774
|
}
|
|
1811
|
-
.atelic-ui .op-shot a {
|
|
1812
|
-
cursor: auto;
|
|
1813
|
-
}
|
|
1814
1775
|
.atelic-ui .op-twin {
|
|
1815
1776
|
display: flex;
|
|
1816
1777
|
align-items: center;
|
|
@@ -1851,17 +1812,17 @@
|
|
|
1851
1812
|
aligned; the screen's column sizes to the picture, a phone crop
|
|
1852
1813
|
about 260px and a wide card up to 45% of the row. A finding
|
|
1853
1814
|
with no screen takes the full width.
|
|
1854
|
-
- Print: one letter sheet. The chrome steps aside (
|
|
1855
|
-
|
|
1856
|
-
|
|
1815
|
+
- Print: one letter sheet. The chrome steps aside (ui's artifact.css
|
|
1816
|
+
hides the header and the credit band on any artifact page), the
|
|
1817
|
+
type comes down, and the screens shrink. The note sets
|
|
1857
1818
|
no @page of its own: the writeup's, the later one in this sheet,
|
|
1858
1819
|
sets letter paper for every artifact page.
|
|
1859
1820
|
|
|
1860
1821
|
Tokens only. The finding is the shared Callout (@atelic-action/ui's components.css),
|
|
1861
|
-
the screen the
|
|
1822
|
+
the screen the shared .op-shot, the sign off the shared .op-who;
|
|
1862
1823
|
this sheet sets the letter around them and their sizes here.
|
|
1863
1824
|
================================================================= */
|
|
1864
|
-
/* The
|
|
1825
|
+
/* The shared .op-shot reads the writeup's own two tokens, mapped here as
|
|
1865
1826
|
the writeup's .op root maps them. */
|
|
1866
1827
|
.atelic-ui .nt {
|
|
1867
1828
|
--op-card: var(--card);
|
|
@@ -2197,11 +2158,12 @@
|
|
|
2197
2158
|
THE READ: the baseline's grades explained by cause (Read.tsx). The
|
|
2198
2159
|
writeup's sibling: it sits inside the writeup's own .op root, so the
|
|
2199
2160
|
column, the head, the grade chip and its key, the tier heading, the
|
|
2200
|
-
section card, the screen, the
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2161
|
+
section card, the screen, the close, and every print rule of the
|
|
2162
|
+
writeup's are its own already. This block adds only what the read has
|
|
2163
|
+
and the writeup does not: the gist table, the beats of a section (the
|
|
2164
|
+
tools we looked through, what they showed with its facts and
|
|
2165
|
+
examples, why, today and after, and what we recommend), the proof
|
|
2166
|
+
pictures in a row, and the numbered plan. It sets no @page; the writeup's, earlier in this sheet and the
|
|
2205
2167
|
later of the two there, sets letter paper for every artifact page.
|
|
2206
2168
|
|
|
2207
2169
|
Tokens only (theme.css).
|
|
@@ -2288,7 +2250,7 @@
|
|
|
2288
2250
|
min-width: 0;
|
|
2289
2251
|
}
|
|
2290
2252
|
|
|
2291
|
-
/* ---- a section: the trade name over the question, then
|
|
2253
|
+
/* ---- a section: the trade name over the question, then its beats ---- */
|
|
2292
2254
|
.atelic-ui .rd-name {
|
|
2293
2255
|
margin: 0 0 4px;
|
|
2294
2256
|
font-family: var(--font-mono);
|
|
@@ -2304,7 +2266,7 @@
|
|
|
2304
2266
|
font-size: 21px;
|
|
2305
2267
|
}
|
|
2306
2268
|
.atelic-ui .rd-beat {
|
|
2307
|
-
margin-top:
|
|
2269
|
+
margin-top: 20px;
|
|
2308
2270
|
min-width: 0;
|
|
2309
2271
|
}
|
|
2310
2272
|
.atelic-ui .rd-beat h4 {
|
|
@@ -2324,12 +2286,99 @@
|
|
|
2324
2286
|
.atelic-ui .rd-beat p:last-child {
|
|
2325
2287
|
margin-bottom: 0;
|
|
2326
2288
|
}
|
|
2289
|
+
/* Where we looked: the tools in a row at the top of the section, each a
|
|
2290
|
+
link with its line under it. */
|
|
2291
|
+
.atelic-ui .rd-tools-list {
|
|
2292
|
+
list-style: none;
|
|
2293
|
+
margin: 0;
|
|
2294
|
+
padding: 0;
|
|
2295
|
+
display: flex;
|
|
2296
|
+
flex-wrap: wrap;
|
|
2297
|
+
gap: 10px 28px;
|
|
2298
|
+
}
|
|
2299
|
+
.atelic-ui .rd-tools-list li {
|
|
2300
|
+
display: flex;
|
|
2301
|
+
flex-direction: column;
|
|
2302
|
+
gap: 1px;
|
|
2303
|
+
min-width: 0;
|
|
2304
|
+
max-width: 100%;
|
|
2305
|
+
}
|
|
2306
|
+
.atelic-ui .rd-tools-list a,
|
|
2307
|
+
.atelic-ui .rd-tool-name {
|
|
2308
|
+
display: inline-flex;
|
|
2309
|
+
align-items: center;
|
|
2310
|
+
gap: 3px;
|
|
2311
|
+
font-size: 15px;
|
|
2312
|
+
font-weight: 600;
|
|
2313
|
+
color: var(--op-accent);
|
|
2314
|
+
text-decoration: none;
|
|
2315
|
+
}
|
|
2316
|
+
.atelic-ui .rd-tool-name {
|
|
2317
|
+
color: var(--op-ink);
|
|
2318
|
+
}
|
|
2319
|
+
.atelic-ui .rd-tools-list a:hover {
|
|
2320
|
+
text-decoration: underline;
|
|
2321
|
+
text-underline-offset: 2px;
|
|
2322
|
+
}
|
|
2323
|
+
.atelic-ui .rd-tools-list svg {
|
|
2324
|
+
flex: none;
|
|
2325
|
+
}
|
|
2326
|
+
.atelic-ui .rd-tool-note {
|
|
2327
|
+
font-size: 13px;
|
|
2328
|
+
line-height: 1.35;
|
|
2329
|
+
color: var(--op-ink-faint);
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
/* What it showed: the facts as a compact table of label and value, in
|
|
2333
|
+
the writeup's table dress (an ink rule over thin row rules). */
|
|
2334
|
+
.atelic-ui .rd-facts {
|
|
2335
|
+
width: 100%;
|
|
2336
|
+
max-width: 620px;
|
|
2337
|
+
margin: 12px 0 0;
|
|
2338
|
+
border-collapse: collapse;
|
|
2339
|
+
border-top: 1.5px solid var(--op-ink);
|
|
2340
|
+
font-size: 15px;
|
|
2341
|
+
line-height: 1.4;
|
|
2342
|
+
}
|
|
2343
|
+
.atelic-ui .rd-facts th,
|
|
2344
|
+
.atelic-ui .rd-facts td {
|
|
2345
|
+
padding: 7px 14px 7px 0;
|
|
2346
|
+
border-bottom: 1px solid var(--neutral-100);
|
|
2347
|
+
text-align: left;
|
|
2348
|
+
vertical-align: top;
|
|
2349
|
+
}
|
|
2350
|
+
.atelic-ui .rd-facts th {
|
|
2351
|
+
width: 55%;
|
|
2352
|
+
font-weight: 400;
|
|
2353
|
+
color: var(--op-ink-soft);
|
|
2354
|
+
}
|
|
2355
|
+
.atelic-ui .rd-facts td {
|
|
2356
|
+
padding-right: 0;
|
|
2357
|
+
font-weight: 600;
|
|
2358
|
+
color: var(--op-ink);
|
|
2359
|
+
font-variant-numeric: tabular-nums;
|
|
2360
|
+
}
|
|
2361
|
+
.atelic-ui .rd-facts tr:last-child > * {
|
|
2362
|
+
border-bottom: 0;
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
/* The examples: a list or more, each under its own short heading. */
|
|
2366
|
+
.atelic-ui .rd-examples-block {
|
|
2367
|
+
margin-top: 14px;
|
|
2368
|
+
max-width: 68ch;
|
|
2369
|
+
}
|
|
2370
|
+
.atelic-ui .rd-examples-block h5 {
|
|
2371
|
+
margin: 0 0 4px;
|
|
2372
|
+
font-size: 15px;
|
|
2373
|
+
font-weight: 650;
|
|
2374
|
+
color: var(--op-ink);
|
|
2375
|
+
}
|
|
2327
2376
|
.atelic-ui .rd-examples {
|
|
2328
2377
|
list-style: disc;
|
|
2329
|
-
margin:
|
|
2378
|
+
margin: 0;
|
|
2330
2379
|
padding-left: 1.2em;
|
|
2331
|
-
max-width: 68ch;
|
|
2332
2380
|
color: var(--op-ink-soft);
|
|
2381
|
+
text-wrap: pretty;
|
|
2333
2382
|
}
|
|
2334
2383
|
.atelic-ui .rd-examples li + li {
|
|
2335
2384
|
margin-top: 4px;
|
|
@@ -2355,6 +2404,92 @@
|
|
|
2355
2404
|
gap: 14px;
|
|
2356
2405
|
margin-top: 14px;
|
|
2357
2406
|
}
|
|
2407
|
+
/* A lone picture keeps a screen's size rather than filling the card. */
|
|
2408
|
+
.atelic-ui .rd-shots[data-count="1"] {
|
|
2409
|
+
max-width: 380px;
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
/* Today and after: what a thing is, and what it becomes. Two columns
|
|
2413
|
+
wide; on a phone each row stacks, each half wearing its label. */
|
|
2414
|
+
.atelic-ui .rd-changes-table {
|
|
2415
|
+
width: 100%;
|
|
2416
|
+
border-collapse: collapse;
|
|
2417
|
+
font-size: 15px;
|
|
2418
|
+
line-height: 1.45;
|
|
2419
|
+
}
|
|
2420
|
+
.atelic-ui .rd-changes-table thead {
|
|
2421
|
+
position: absolute;
|
|
2422
|
+
width: 1px;
|
|
2423
|
+
height: 1px;
|
|
2424
|
+
overflow: hidden;
|
|
2425
|
+
clip: rect(0, 0, 0, 0);
|
|
2426
|
+
white-space: nowrap;
|
|
2427
|
+
}
|
|
2428
|
+
.atelic-ui .rd-changes-table,
|
|
2429
|
+
.atelic-ui .rd-changes-table tbody,
|
|
2430
|
+
.atelic-ui .rd-changes-table tr,
|
|
2431
|
+
.atelic-ui .rd-changes-table td {
|
|
2432
|
+
display: block;
|
|
2433
|
+
}
|
|
2434
|
+
.atelic-ui .rd-changes-table tbody tr {
|
|
2435
|
+
padding: 10px 0;
|
|
2436
|
+
border-bottom: 1px solid var(--neutral-100);
|
|
2437
|
+
}
|
|
2438
|
+
.atelic-ui .rd-changes-table tbody tr:first-child {
|
|
2439
|
+
border-top: 1.5px solid var(--op-ink);
|
|
2440
|
+
}
|
|
2441
|
+
.atelic-ui .rd-changes-table tbody tr:last-child {
|
|
2442
|
+
border-bottom: 0;
|
|
2443
|
+
padding-bottom: 0;
|
|
2444
|
+
}
|
|
2445
|
+
.atelic-ui .rd-changes-table td {
|
|
2446
|
+
text-align: left;
|
|
2447
|
+
text-wrap: pretty;
|
|
2448
|
+
}
|
|
2449
|
+
.atelic-ui .rd-changes-table td + td {
|
|
2450
|
+
margin-top: 6px;
|
|
2451
|
+
}
|
|
2452
|
+
.atelic-ui .rd-changes-table td::before {
|
|
2453
|
+
content: attr(data-label);
|
|
2454
|
+
display: block;
|
|
2455
|
+
margin-bottom: 1px;
|
|
2456
|
+
font-family: var(--font-mono);
|
|
2457
|
+
font-size: 11px;
|
|
2458
|
+
letter-spacing: 0.1em;
|
|
2459
|
+
text-transform: uppercase;
|
|
2460
|
+
color: var(--op-ink-faint);
|
|
2461
|
+
}
|
|
2462
|
+
.atelic-ui .rd-changes-table td.rd-now {
|
|
2463
|
+
color: var(--op-ink-soft);
|
|
2464
|
+
}
|
|
2465
|
+
.atelic-ui .rd-changes-table td.rd-after {
|
|
2466
|
+
color: var(--op-ink);
|
|
2467
|
+
}
|
|
2468
|
+
.atelic-ui .rd-changes-table td.rd-after::before {
|
|
2469
|
+
color: var(--op-accent);
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
/* What we recommend: a checklist, each item led by the accent's check. */
|
|
2473
|
+
.atelic-ui .rd-checks {
|
|
2474
|
+
list-style: none;
|
|
2475
|
+
margin: 0;
|
|
2476
|
+
padding: 0;
|
|
2477
|
+
display: grid;
|
|
2478
|
+
gap: 8px;
|
|
2479
|
+
max-width: 68ch;
|
|
2480
|
+
}
|
|
2481
|
+
.atelic-ui .rd-checks li {
|
|
2482
|
+
display: grid;
|
|
2483
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
2484
|
+
column-gap: 10px;
|
|
2485
|
+
align-items: start;
|
|
2486
|
+
line-height: 1.45;
|
|
2487
|
+
text-wrap: pretty;
|
|
2488
|
+
}
|
|
2489
|
+
.atelic-ui .rd-checks svg {
|
|
2490
|
+
margin-top: 0.2em;
|
|
2491
|
+
color: var(--op-accent);
|
|
2492
|
+
}
|
|
2358
2493
|
|
|
2359
2494
|
/* ---- the plan: the steps in order, numbered ---- */
|
|
2360
2495
|
.atelic-ui .rd-plan {
|
|
@@ -2448,6 +2583,69 @@
|
|
|
2448
2583
|
padding-left: 20px;
|
|
2449
2584
|
padding-right: 16px;
|
|
2450
2585
|
}
|
|
2586
|
+
.atelic-ui .rd-facts th {
|
|
2587
|
+
width: 45%;
|
|
2588
|
+
}
|
|
2589
|
+
.atelic-ui .rd-changes-table {
|
|
2590
|
+
display: table;
|
|
2591
|
+
table-layout: fixed;
|
|
2592
|
+
}
|
|
2593
|
+
.atelic-ui .rd-changes-table thead {
|
|
2594
|
+
display: table-header-group;
|
|
2595
|
+
position: static;
|
|
2596
|
+
width: auto;
|
|
2597
|
+
height: auto;
|
|
2598
|
+
overflow: visible;
|
|
2599
|
+
clip: auto;
|
|
2600
|
+
}
|
|
2601
|
+
.atelic-ui .rd-changes-table tbody {
|
|
2602
|
+
display: table-row-group;
|
|
2603
|
+
}
|
|
2604
|
+
.atelic-ui .rd-changes-table tr,
|
|
2605
|
+
.atelic-ui .rd-changes-table tbody tr {
|
|
2606
|
+
display: table-row;
|
|
2607
|
+
padding: 0;
|
|
2608
|
+
border: 0;
|
|
2609
|
+
}
|
|
2610
|
+
.atelic-ui .rd-changes-table th,
|
|
2611
|
+
.atelic-ui .rd-changes-table td {
|
|
2612
|
+
display: table-cell;
|
|
2613
|
+
width: 50%;
|
|
2614
|
+
padding: 9px 18px 9px 0;
|
|
2615
|
+
border-bottom: 1px solid var(--neutral-100);
|
|
2616
|
+
text-align: left;
|
|
2617
|
+
vertical-align: top;
|
|
2618
|
+
}
|
|
2619
|
+
.atelic-ui .rd-changes-table th + th,
|
|
2620
|
+
.atelic-ui .rd-changes-table td + td {
|
|
2621
|
+
margin-top: 0;
|
|
2622
|
+
padding-left: 18px;
|
|
2623
|
+
padding-right: 0;
|
|
2624
|
+
border-left: 1px solid var(--neutral-100);
|
|
2625
|
+
}
|
|
2626
|
+
.atelic-ui .rd-changes-table td::before {
|
|
2627
|
+
content: none;
|
|
2628
|
+
}
|
|
2629
|
+
.atelic-ui .rd-changes-table tbody tr:first-child {
|
|
2630
|
+
border-top: 0;
|
|
2631
|
+
}
|
|
2632
|
+
.atelic-ui .rd-changes-table thead th {
|
|
2633
|
+
padding-top: 0;
|
|
2634
|
+
padding-bottom: 8px;
|
|
2635
|
+
border-bottom: 1.5px solid var(--op-ink);
|
|
2636
|
+
font-family: var(--font-mono);
|
|
2637
|
+
font-size: 11.5px;
|
|
2638
|
+
font-weight: 500;
|
|
2639
|
+
letter-spacing: 0.1em;
|
|
2640
|
+
text-transform: uppercase;
|
|
2641
|
+
color: var(--op-ink-faint);
|
|
2642
|
+
}
|
|
2643
|
+
.atelic-ui .rd-changes-table thead th.rd-after {
|
|
2644
|
+
color: var(--op-accent);
|
|
2645
|
+
}
|
|
2646
|
+
.atelic-ui .rd-changes-table tbody tr:last-child td {
|
|
2647
|
+
border-bottom: 0;
|
|
2648
|
+
}
|
|
2451
2649
|
.atelic-ui .rd-shots[data-count="2"] {
|
|
2452
2650
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
2453
2651
|
}
|
|
@@ -2465,6 +2663,10 @@
|
|
|
2465
2663
|
border-color: var(--neutral-200);
|
|
2466
2664
|
}
|
|
2467
2665
|
.atelic-ui .rd-gist-table tr,
|
|
2666
|
+
.atelic-ui .rd-changes-table tr,
|
|
2667
|
+
.atelic-ui .rd-facts,
|
|
2668
|
+
.atelic-ui .rd-tools,
|
|
2669
|
+
.atelic-ui .rd-checks li,
|
|
2468
2670
|
.atelic-ui .rd-closing,
|
|
2469
2671
|
.atelic-ui .rd-why,
|
|
2470
2672
|
.atelic-ui .rd-step,
|
|
@@ -2473,6 +2675,7 @@
|
|
|
2473
2675
|
break-inside: avoid;
|
|
2474
2676
|
}
|
|
2475
2677
|
.atelic-ui .rd-beat h4,
|
|
2678
|
+
.atelic-ui .rd-examples-block h5,
|
|
2476
2679
|
.atelic-ui .rd-plan h2 {
|
|
2477
2680
|
break-after: avoid;
|
|
2478
2681
|
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { newTabProps } from "@atelic-action/ui/artifact";
|
|
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
|
-
}
|
package/src/documents/Shot.tsx
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { newTabProps } from "@atelic-action/ui/artifact";
|
|
2
|
-
import { Maximize2 } from "lucide-react";
|
|
3
|
-
|
|
4
|
-
export interface WriteupShot {
|
|
5
|
-
/** The crop shown on the page. */
|
|
6
|
-
src: string;
|
|
7
|
-
alt: string;
|
|
8
|
-
/** The full screenshot the crop opens in a new tab; omit to render the crop alone. */
|
|
9
|
-
href?: string;
|
|
10
|
-
caption: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* The owner's own screen: the crop, opening the full screenshot in a new tab
|
|
15
|
-
* when it has one, over its caption. The writeup and the note both wear it;
|
|
16
|
-
* its styles (.op-shot) are the writeup's, and the note only sizes it. A
|
|
17
|
-
* linked crop says it opens: a hover title, and an expand badge the note
|
|
18
|
-
* shows on its thumbnails and the writeup keeps hidden.
|
|
19
|
-
*/
|
|
20
|
-
export function Shot({ shot }: { shot: WriteupShot }) {
|
|
21
|
-
return (
|
|
22
|
-
<figure className="op-shot">
|
|
23
|
-
{shot.href ? (
|
|
24
|
-
<a href={shot.href} title="Open the full picture" {...newTabProps(shot.href, true)}>
|
|
25
|
-
<img src={shot.src} alt={shot.alt} loading="lazy" />
|
|
26
|
-
<span className="op-shot-open" aria-hidden="true">
|
|
27
|
-
<Maximize2 size={12} />
|
|
28
|
-
</span>
|
|
29
|
-
</a>
|
|
30
|
-
) : (
|
|
31
|
-
<span className="op-shot-frame">
|
|
32
|
-
<img src={shot.src} alt={shot.alt} loading="lazy" />
|
|
33
|
-
</span>
|
|
34
|
-
)}
|
|
35
|
-
<figcaption>{shot.caption}</figcaption>
|
|
36
|
-
</figure>
|
|
37
|
-
);
|
|
38
|
-
}
|