@atelic-action/artifact 5.0.0 → 5.1.1
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 +2 -2
- package/package.json +3 -3
- package/src/documents/TheRead.tsx +2 -2
- package/src/documents/Topic.tsx +156 -3
- package/src/documents/documents.css +52 -7
- package/src/documents/index.ts +4 -0
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ Every export of the package, all from the one entry, `@atelic-action/artifact`.
|
|
|
26
26
|
| `ReadAccess`, `ReadTier`, `ReadSection`, `ReadChange`, `ReadRecommendation`, `ReadMarkets`, `ReadMarketRow`, `ReadRented`, `ReadStep`, `ReadPlan`, `ReadClose`, `ReadPrint`, `ReadLink`, `ReadParagraph` | The shapes a read's content is written in. `ReadSection` is a `ReadTopic` plus `gist`, `before`, `changes`, and `recommend`; its `why`, a finding's `body`, a market row's `read`, and the rented `text` take a paragraph (a string, or runs of text and links) as well as a node, so a routine can write a read as JSON | none |
|
|
27
27
|
| `Topic` | The section card on its own: the trade name linked to its definition, the one liner, the grade, the why line, the blocks, and the sources, with an `extra` slot after the blocks. The read renders every section through it | `/the-read` |
|
|
28
28
|
| `ratingCell` | A star rating table cell for a `table` block, sorted by its figure | `/the-read` |
|
|
29
|
-
| `ReadTopic`, `ReadBlock`, `ReadInnerBlock`, `ReadChecks`, `ReadTable`, `ReadBar`, `ReadScale`, `ReadStats`, `ReadColumns`, `ReadFindings`, `ReadShots`, `ReadMore`, `ReadBullet`, `ReadSource`, `ReadMark`, `ReadCell`, `ReadSorted`, `ReadTone` | The shapes a section card and its blocks are written in | none |
|
|
29
|
+
| `ReadTopic`, `ReadBlock`, `ReadInnerBlock`, `ReadChecks`, `ReadTable`, `ReadBar`, `ReadScale`, `ReadStats`, `ReadColumns`, `ReadFindings`, `ReadShots`, `ReadMore`, `ReadBullet`, `ReadSource`, `ReadMark`, `ReadCell`, `ReadSorted`, `ReadTone` | The shapes a section card and its blocks are written in. In a `table` block, a cell past the first given as `null` is empty: it reaches ui's `DataTable` untouched, draws its short gray rule, reads the column's `emptyLabel` (or "None") to a screen reader, and sorts last either way, while a typed dash stays content, since it can mean unchanged. A `none` mark draws the same rule with its own `text` but sorts as a mark, lowest of the four, and the markets table's unread grade draws it too | none |
|
|
30
30
|
| `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 under ui's names | 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 |
|
|
@@ -48,7 +48,7 @@ What a document is built from stays in `@atelic-action/ui`, and this package imp
|
|
|
48
48
|
bun add @atelic-action/artifact @atelic-action/ui
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
Peer dependencies: `@atelic-action/ui` at `^3.
|
|
51
|
+
Peer dependencies: `@atelic-action/ui` at `^3.9.1`, `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.
|
|
52
52
|
|
|
53
53
|
```tsx
|
|
54
54
|
import { TheRead } from "@atelic-action/artifact";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atelic-action/artifact",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.1.1",
|
|
4
4
|
"description": "The composed documents the Atelic practice hands or sends a business (the read, the monthly page, the note, the first call sheet, and the sealed document), built from the pieces in @atelic-action/ui",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,12 +37,12 @@
|
|
|
37
37
|
"format": "biome format . --write"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"@atelic-action/ui": "^3.
|
|
40
|
+
"@atelic-action/ui": "^3.9.1",
|
|
41
41
|
"lucide-react": "^1.23.0",
|
|
42
42
|
"react": "^19.0.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@atelic-action/ui": "^3.
|
|
45
|
+
"@atelic-action/ui": "^3.9.1",
|
|
46
46
|
"@biomejs/biome": "^2.0.0",
|
|
47
47
|
"@playwright/test": "^1.60.0",
|
|
48
48
|
"@sentry/react": "^10.0.0",
|
|
@@ -454,8 +454,8 @@ function Markets({ markets }: { markets: ReadMarkets }) {
|
|
|
454
454
|
</>
|
|
455
455
|
) : (
|
|
456
456
|
<>
|
|
457
|
-
<span className="
|
|
458
|
-
<span className="
|
|
457
|
+
<span className="data-table-empty" aria-hidden="true" />
|
|
458
|
+
<span className="data-table-empty-label">{`${column}, not read`}</span>
|
|
459
459
|
</>
|
|
460
460
|
)}
|
|
461
461
|
</td>
|
package/src/documents/Topic.tsx
CHANGED
|
@@ -82,7 +82,15 @@ export interface ReadSorted {
|
|
|
82
82
|
sort: number | string;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
/**
|
|
86
|
+
* A table cell: a node, a mark, or a sorted pair. `null` past the first
|
|
87
|
+
* column (which names the row, so is never empty) is an empty cell,
|
|
88
|
+
* passed to the shared table untouched: it draws the table's short gray rule,
|
|
89
|
+
* reads its column's `emptyLabel` to a screen reader, and sorts last either
|
|
90
|
+
* way. A typed dash is a string and stays content, since a dash can mean
|
|
91
|
+
* "unchanged".
|
|
92
|
+
*/
|
|
93
|
+
export type ReadCell = ReactNode | ReadMark | ReadSorted | null;
|
|
86
94
|
|
|
87
95
|
function isMark(cell: ReadCell): cell is ReadMark {
|
|
88
96
|
return typeof cell === "object" && cell !== null && "mark" in cell;
|
|
@@ -112,6 +120,8 @@ export interface ReadTable extends BlockBase {
|
|
|
112
120
|
firstSort?: "asc" | "desc";
|
|
113
121
|
/** The column hugs its content, for a swatch or a mark. */
|
|
114
122
|
fit?: boolean;
|
|
123
|
+
/** What a screen reader hears for an empty cell in this column; defaults to "None". */
|
|
124
|
+
emptyLabel?: string;
|
|
115
125
|
}>;
|
|
116
126
|
rows: Array<{ cells: ReadCell[]; tone?: ReadTone }>;
|
|
117
127
|
/** The sort the table opens in. */
|
|
@@ -133,6 +143,84 @@ export interface ReadScale extends BlockBase {
|
|
|
133
143
|
readings: ThresholdReading[];
|
|
134
144
|
}
|
|
135
145
|
|
|
146
|
+
/** One run of Google's phone test: the five figures its report leads with, as the API returns them. */
|
|
147
|
+
export interface ReadSpeedRun {
|
|
148
|
+
/** The performance score, 0 to 100. */
|
|
149
|
+
score: number;
|
|
150
|
+
/** First Contentful Paint, in milliseconds. */
|
|
151
|
+
fcpMs: number;
|
|
152
|
+
/** Largest Contentful Paint, in milliseconds. */
|
|
153
|
+
lcpMs: number;
|
|
154
|
+
/** Total Blocking Time, in milliseconds. */
|
|
155
|
+
tbtMs: number;
|
|
156
|
+
/** Cumulative Layout Shift, unitless. */
|
|
157
|
+
cls: number;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** A column of the speed table: its heading and the explainer behind its info icon. */
|
|
161
|
+
export interface ReadSpeedColumn {
|
|
162
|
+
label: string;
|
|
163
|
+
hint: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** The words the speed block uses, for a document in another register. */
|
|
167
|
+
export interface ReadSpeedLabels {
|
|
168
|
+
/** What a screen reader hears before a run's grade letter. */
|
|
169
|
+
gradeWord: string;
|
|
170
|
+
score: ReadSpeedColumn;
|
|
171
|
+
firstPaint: ReadSpeedColumn;
|
|
172
|
+
mainContent: ReadSpeedColumn;
|
|
173
|
+
frozen: ReadSpeedColumn;
|
|
174
|
+
shift: ReadSpeedColumn;
|
|
175
|
+
/** The small callout over the table: why there are three runs. */
|
|
176
|
+
whyThreeTitle: string;
|
|
177
|
+
whyThree: string;
|
|
178
|
+
/** The words before the date the runs were made, e.g. "These ran on". */
|
|
179
|
+
ranOn: string;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export const readSpeedLabels: ReadSpeedLabels = {
|
|
183
|
+
gradeWord: "Grade",
|
|
184
|
+
score: {
|
|
185
|
+
label: "Score",
|
|
186
|
+
hint: "Google’s own mark for the page, out of 100, from the four measures beside it. We grade it the way a school grades: 90 is an A, 80 a B, 60 a C, and under 60 a D.",
|
|
187
|
+
},
|
|
188
|
+
firstPaint: {
|
|
189
|
+
label: "First paint",
|
|
190
|
+
hint: "The first moment anything appears on the screen. Google calls it First Contentful Paint and draws its line for good at 1.8 s.",
|
|
191
|
+
},
|
|
192
|
+
mainContent: {
|
|
193
|
+
label: "Main content",
|
|
194
|
+
hint: "When the biggest thing on the screen, usually the headline or the photo behind it, has arrived. Google calls it Largest Contentful Paint and draws its line at 2.5 s.",
|
|
195
|
+
},
|
|
196
|
+
frozen: {
|
|
197
|
+
label: "Frozen for",
|
|
198
|
+
hint: "How long the page ignores a tap or a scroll while its scripts run. Google calls it Total Blocking Time and draws its line at 0.2 s.",
|
|
199
|
+
},
|
|
200
|
+
shift: {
|
|
201
|
+
label: "Layout shift",
|
|
202
|
+
hint: "How much the page jumps around while it loads, so a button moves as a thumb reaches for it. Google calls it Cumulative Layout Shift and draws its line at 0.1.",
|
|
203
|
+
},
|
|
204
|
+
whyThreeTitle: "Why three runs.",
|
|
205
|
+
whyThree:
|
|
206
|
+
"A single run swings with Google’s own servers and the moment it ran, so we run the test three times a minute apart. The middle score is the reading; the best and the worst are the range.",
|
|
207
|
+
ranOn: "These ran on",
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Google's phone test: three runs, the five figures each, listed best score
|
|
212
|
+
* first; a small callout over the table says the middle one is the reading. The letter
|
|
213
|
+
* beside each score and the row's tone follow the score, graded the way a
|
|
214
|
+
* school grades (FOOTPRINT.md). The figures are the snapshot's, never typed.
|
|
215
|
+
*/
|
|
216
|
+
export interface ReadSpeed extends BlockBase {
|
|
217
|
+
kind: "speed";
|
|
218
|
+
runs: ReadSpeedRun[];
|
|
219
|
+
/** The day the runs were made, ISO, for the callout under the table. */
|
|
220
|
+
ran?: string;
|
|
221
|
+
labels?: Partial<ReadSpeedLabels>;
|
|
222
|
+
}
|
|
223
|
+
|
|
136
224
|
/** A few numbers worth reading first, each with its period and its comparisons. */
|
|
137
225
|
export interface ReadStats extends BlockBase {
|
|
138
226
|
kind: "stats";
|
|
@@ -167,6 +255,7 @@ export type ReadInnerBlock =
|
|
|
167
255
|
| ReadTable
|
|
168
256
|
| ReadBar
|
|
169
257
|
| ReadScale
|
|
258
|
+
| ReadSpeed
|
|
170
259
|
| ReadFindings
|
|
171
260
|
| ReadStats
|
|
172
261
|
| ReadColumns
|
|
@@ -222,8 +311,8 @@ function Mark({ cell }: { cell: ReadMark }) {
|
|
|
222
311
|
if (cell.mark === "none") {
|
|
223
312
|
return (
|
|
224
313
|
<span className="op-mark-cell" data-tone="muted">
|
|
225
|
-
<span className="
|
|
226
|
-
<span className="
|
|
314
|
+
<span className="data-table-empty" aria-hidden="true" />
|
|
315
|
+
<span className="data-table-empty-label">{cell.text}</span>
|
|
227
316
|
</span>
|
|
228
317
|
);
|
|
229
318
|
}
|
|
@@ -292,6 +381,68 @@ function Table({ block }: { block: ReadTable }) {
|
|
|
292
381
|
);
|
|
293
382
|
}
|
|
294
383
|
|
|
384
|
+
/** The letter for a score on a 100 point scale, graded the way a school grades. */
|
|
385
|
+
function scoreGrade(score: number): WriteupGrade {
|
|
386
|
+
if (score >= 90) return "a";
|
|
387
|
+
if (score >= 80) return "b";
|
|
388
|
+
if (score >= 60) return "c";
|
|
389
|
+
return "d";
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const gradeTone: Record<WriteupGrade, ReadTone> = { a: "good", b: "info", c: "warn", d: "bad" };
|
|
393
|
+
|
|
394
|
+
const seconds = (ms: number) => `${(ms / 1000).toFixed(1)} s`;
|
|
395
|
+
const shift = (cls: number) => (cls < 0.005 ? "0" : cls.toFixed(2));
|
|
396
|
+
|
|
397
|
+
function Speed({ block }: { block: ReadSpeed }) {
|
|
398
|
+
const labels = { ...readSpeedLabels, ...block.labels };
|
|
399
|
+
const sorted = [...block.runs].sort((a, b) => b.score - a.score);
|
|
400
|
+
const rows = sorted.map((run) => {
|
|
401
|
+
const grade = scoreGrade(run.score);
|
|
402
|
+
return {
|
|
403
|
+
tone: gradeTone[grade],
|
|
404
|
+
cells: [
|
|
405
|
+
<span className="op-speed-score" key="score">
|
|
406
|
+
<span className="op-sr-only">{`${labels.gradeWord} ${gradeLabel[grade]}`}</span>
|
|
407
|
+
<span aria-hidden="true">
|
|
408
|
+
<GradeChip grade={grade} />
|
|
409
|
+
</span>
|
|
410
|
+
{run.score}
|
|
411
|
+
</span>,
|
|
412
|
+
seconds(run.fcpMs),
|
|
413
|
+
seconds(run.lcpMs),
|
|
414
|
+
seconds(run.tbtMs),
|
|
415
|
+
shift(run.cls),
|
|
416
|
+
],
|
|
417
|
+
};
|
|
418
|
+
});
|
|
419
|
+
const column = (c: ReadSpeedColumn) => ({
|
|
420
|
+
label: c.label,
|
|
421
|
+
hint: c.hint,
|
|
422
|
+
align: "right" as const,
|
|
423
|
+
});
|
|
424
|
+
return (
|
|
425
|
+
<>
|
|
426
|
+
<Callout tone="muted" title={labels.whyThreeTitle} className="op-speed-why">
|
|
427
|
+
{labels.whyThree}
|
|
428
|
+
{block.ran && ` ${labels.ranOn} ${block.ran}.`}
|
|
429
|
+
</Callout>
|
|
430
|
+
<Block className="op-table op-speed" block={block}>
|
|
431
|
+
<DataTable
|
|
432
|
+
columns={[
|
|
433
|
+
column(labels.score),
|
|
434
|
+
column(labels.firstPaint),
|
|
435
|
+
column(labels.mainContent),
|
|
436
|
+
column(labels.frozen),
|
|
437
|
+
column(labels.shift),
|
|
438
|
+
]}
|
|
439
|
+
rows={rows}
|
|
440
|
+
/>
|
|
441
|
+
</Block>
|
|
442
|
+
</>
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
295
446
|
/** A string or runs of text and links, the shape JSON content arrives in; a node of elements is anything else. */
|
|
296
447
|
export function isRuns(value: unknown): value is ArtifactParagraph {
|
|
297
448
|
if (typeof value === "string") return true;
|
|
@@ -375,6 +526,8 @@ function TopicBlock({ block }: { block: ReadBlock }) {
|
|
|
375
526
|
);
|
|
376
527
|
case "table":
|
|
377
528
|
return <Table block={block} />;
|
|
529
|
+
case "speed":
|
|
530
|
+
return <Speed block={block} />;
|
|
378
531
|
case "bar":
|
|
379
532
|
return (
|
|
380
533
|
<Block className="op-bar" block={block}>
|
|
@@ -1069,13 +1069,6 @@
|
|
|
1069
1069
|
color: var(--op-ink);
|
|
1070
1070
|
font-weight: 600;
|
|
1071
1071
|
}
|
|
1072
|
-
.atelic-ui .op-dash {
|
|
1073
|
-
display: inline-block;
|
|
1074
|
-
width: 14px;
|
|
1075
|
-
height: 1.5px;
|
|
1076
|
-
vertical-align: 0.3em;
|
|
1077
|
-
background: var(--neutral-300);
|
|
1078
|
-
}
|
|
1079
1072
|
|
|
1080
1073
|
/* The blocks are the shared package's components. These are the read's
|
|
1081
1074
|
sizes for them. */
|
|
@@ -1118,6 +1111,58 @@
|
|
|
1118
1111
|
.atelic-ui .op-table td:has(.op-wrap) {
|
|
1119
1112
|
white-space: normal;
|
|
1120
1113
|
}
|
|
1114
|
+
/* the phone test: a small grade chip before each run's score, at the
|
|
1115
|
+
column's left edge on a wide screen */
|
|
1116
|
+
.atelic-ui .op-speed .op-grade {
|
|
1117
|
+
display: inline-grid;
|
|
1118
|
+
place-items: center;
|
|
1119
|
+
width: 22px;
|
|
1120
|
+
height: 22px;
|
|
1121
|
+
margin: 0 8px 0 0;
|
|
1122
|
+
padding: 0;
|
|
1123
|
+
font-size: 12px;
|
|
1124
|
+
}
|
|
1125
|
+
.atelic-ui .op-speed-score {
|
|
1126
|
+
display: inline-flex;
|
|
1127
|
+
align-items: center;
|
|
1128
|
+
font-variant-numeric: tabular-nums;
|
|
1129
|
+
}
|
|
1130
|
+
.atelic-ui .op-speed tbody th {
|
|
1131
|
+
white-space: nowrap;
|
|
1132
|
+
font-weight: 600;
|
|
1133
|
+
}
|
|
1134
|
+
@media print, (min-width: 721px) {
|
|
1135
|
+
.atelic-ui .op-speed-score {
|
|
1136
|
+
display: flex;
|
|
1137
|
+
justify-content: space-between;
|
|
1138
|
+
width: 100%;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
/* the why over the table: one small line, the title run into the body */
|
|
1142
|
+
.atelic-ui .op-speed-why {
|
|
1143
|
+
margin-top: 12px;
|
|
1144
|
+
font-size: 13px;
|
|
1145
|
+
line-height: 1.45;
|
|
1146
|
+
color: var(--op-ink-soft);
|
|
1147
|
+
}
|
|
1148
|
+
.atelic-ui .op-speed-why .callout-icon {
|
|
1149
|
+
width: 14px;
|
|
1150
|
+
height: 14px;
|
|
1151
|
+
margin-top: 2px;
|
|
1152
|
+
}
|
|
1153
|
+
.atelic-ui .op-speed-why .callout-title,
|
|
1154
|
+
.atelic-ui .op-speed-why .callout-body {
|
|
1155
|
+
display: inline;
|
|
1156
|
+
font-size: inherit;
|
|
1157
|
+
line-height: inherit;
|
|
1158
|
+
}
|
|
1159
|
+
.atelic-ui .op-speed-why .callout-title {
|
|
1160
|
+
font-weight: 600;
|
|
1161
|
+
color: var(--op-ink);
|
|
1162
|
+
}
|
|
1163
|
+
.atelic-ui .op-speed-why .callout-title::after {
|
|
1164
|
+
content: " ";
|
|
1165
|
+
}
|
|
1121
1166
|
.atelic-ui .op-mark-cell {
|
|
1122
1167
|
display: inline-flex;
|
|
1123
1168
|
align-items: center;
|
package/src/documents/index.ts
CHANGED
|
@@ -25,10 +25,14 @@ export {
|
|
|
25
25
|
type ReadShots,
|
|
26
26
|
type ReadSorted,
|
|
27
27
|
type ReadSource,
|
|
28
|
+
type ReadSpeed,
|
|
29
|
+
type ReadSpeedLabels,
|
|
30
|
+
type ReadSpeedRun,
|
|
28
31
|
type ReadStats,
|
|
29
32
|
type ReadTable,
|
|
30
33
|
type ReadTone,
|
|
31
34
|
type ReadTopic,
|
|
32
35
|
ratingCell,
|
|
36
|
+
readSpeedLabels,
|
|
33
37
|
Topic,
|
|
34
38
|
} from "./Topic";
|