@atelic-action/artifact 4.1.0 → 4.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 CHANGED
@@ -2,12 +2,12 @@
2
2
 
3
3
  The documents the practice hands or sends a business, as a published package and as the site a business's folder is cut from. This repo is two things at once:
4
4
 
5
- - **The package**, `@atelic-action/artifact` on npm: the composed documents (the writeup, the read, the monthly page, the note, and the sealed document), each with its prop types and its default labels, and the stylesheet they wear. It ships `src/documents/` and nothing else, as source, the way `@atelic-action/ui` does, and depends on that package for every piece a document is built from.
5
+ - **The package**, `@atelic-action/artifact` on npm: the composed documents (the writeup, the read, the monthly page, the note, the first call sheet, and the sealed document), each with its prop types and its default labels, and the stylesheet they wear. It ships `src/documents/` and nothing else, as source, the way `@atelic-action/ui` does, and depends on that package for every piece a document is built from.
6
6
  - **The seed**: the routes, the content stubs, the config, the stub files the host finds by path, and the tests a new folder of `atelic-action/artifacts` is cut from, and the template site that shows every document on fictional content.
7
7
 
8
8
  Three layers, each a version the next depends on (ATE-671): `@atelic-action/ui` holds the pieces, this package holds the documents built from them, and each business's folder holds only its content, routes, and config. A document fix reaches a folder by bumping this package; a piece fix by bumping ui here, then this package in the folder.
9
9
 
10
- Five artifacts live in the template site: the **writeup** at `/writeup`, the graded document an owner gets, a web page first with its printed copy derived from it; the **read** at `/the-read`, what a customer gets at the end of an engagement's first week, the baseline's grades explained by cause, with the plan in order between the gist and the cards; the **proposal** at `/proposal`, sealed into the page and opened by the reader's own email address; the **monthly page** at `/report`, what an Operate client gets each month on one letter sheet; and the **note** at `/notes`, the first touch for a business whose site publishes no email address, set as a short page. The bare host sends a reader to the writeup. Every artifact prerenders to static HTML, stays out of every index, and carries the practice's brand by default.
10
+ Six artifacts live in the template site: the **writeup** at `/writeup`, the graded document an owner gets, a web page first with its printed copy derived from it; the **read** at `/the-read`, what a customer gets at the end of an engagement's first week, the baseline's grades explained by cause, with the plan in order between the gist and the cards; the **proposal** at `/proposal`, sealed into the page and opened by the reader's own email address; the **monthly page** at `/report`, what an Operate client gets each month on one letter sheet; the **note** at `/notes`, the first touch for a business whose site publishes no email address, set as a short page; and the **first call sheet** at `/first-call`, the desk's page for the first call with a business that booked off a cold first touch, used to guide the call and not sent for now, written so it could be shared with them later. The bare host sends a reader to the writeup. Every artifact prerenders to static HTML, stays out of every index, and carries the practice's brand by default.
11
11
 
12
12
  The method behind them (the seven sections, the four grades from A to D, the rented line, what moved, and when an artifact is private) is the practice repo's `FOOTPRINT.md`. This repo builds to it and never restates it.
13
13
 
@@ -33,6 +33,10 @@ Every export of the package, all from the one entry, `@atelic-action/artifact`.
33
33
  | `ReportLead`, `ReportLeads`, `ReportGradeRow` | The shapes a monthly page's content is written in | none |
34
34
  | `Note`, `NoteProps` | The first touch set as a page: the opener, two lanes of findings, where the work would start, the close | `/notes` |
35
35
  | `NoteLane`, `NoteFinding`, `NoteStart`, `NoteStep`, `NoteClose`, `NoteLink`, `NoteParagraph` | The shapes a note's content is written in | none |
36
+ | `FirstCall`, `FirstCallProps` | The desk's sheet for the first call with a business that booked off a cold first touch: the brief with a picture beside it, the open as numbered steps, one unit per topic (a TL;DR, then show, ask, and move, each show line linked live with its picture opening the full capture), and the close, which always ends on who else decides. Not sent for now, but open like the host and shareable later, so it carries no desk instruction; it lives beside the writeup | `/first-call` |
37
+ | `firstCallLabels`, `FirstCallLabels` | The fixed words the sheet sets (Show, Ask, Move, the close's who else decides beat, and the rest), each overridable through `labels` | `/first-call` |
38
+ | `firstCallSectionIds` | The ids the page takes for itself (the shell's `main`, the brief, the open, the close), which a unit's `id` must not repeat; the content test checks it | none |
39
+ | `FirstCallFact`, `FirstCallStep`, `FirstCallShow`, `FirstCallUnit`, `FirstCallParagraph` | The shapes a first call sheet's content is written in, all plain data, so a skill writes the sheet as JSON | none |
36
40
  | `Gate`, `GateProps`, `GatePayload`, `GatePerson` | The sealed document, opened in the browser by the reader's own address, and the payload it opens | `/proposal` |
37
41
  | `normalizeEmail` | How a typed address becomes the key a payload opens with; the sealing command mirrors it | none |
38
42
  | `documents.css` | The documents' styles, at `@atelic-action/artifact/styles/documents.css` | every route |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@atelic-action/artifact",
3
- "version": "4.1.0",
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",
3
+ "version": "4.2.0",
4
+ "description": "The composed documents the Atelic practice hands or sends a business (the writeup, 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",
7
7
  "repository": {
@@ -0,0 +1,325 @@
1
+ import {
2
+ type ArtifactParagraph,
3
+ ArtifactAnchor as FirstCallAnchor,
4
+ Runs,
5
+ Shot,
6
+ type WriteupShot,
7
+ } from "@atelic-action/ui/artifact";
8
+ import { Callout, IconList } from "@atelic-action/ui/components";
9
+ import { Chip } from "@atelic-action/ui/primitives";
10
+ import { ArrowRight, MessageCircleQuestion } from "lucide-react";
11
+ import type { ReactNode } from "react";
12
+
13
+ /**
14
+ * The first call sheet: what the practice scans during the first call with a
15
+ * business that booked off a cold first touch. The brief, the open, then one
16
+ * unit per topic, each a show, an ask, and a move, then the close. It lives
17
+ * at `<short>.atelic.me/first-call` beside the writeup it walks.
18
+ *
19
+ * The practice uses it to guide the call and does not send it for now, but
20
+ * it is open like the rest of the host and may be shared with the business
21
+ * down the line, so it is written as a page they could read: everything
22
+ * on it is something we would show them or say to them, and it carries no
23
+ * desk instruction, no tracking, no prices, and no budget script. The method
24
+ * behind it (what goes in the brief, the open's three beats, which unit goes
25
+ * first, the posture in the room) is the practice repo's
26
+ * Outreach/discovery.md, The First Call Set, and nothing here restates it.
27
+ * The document owns only the shape.
28
+ *
29
+ * It is the writeup's and the read's sibling and wears their classes: the
30
+ * writeup's topic and findings blocks for the brief and each unit's show,
31
+ * the read's numbered steps for the open and the close. Its own few rules
32
+ * sit under fc- in documents.css. Every prop is plain data, so a skill can
33
+ * write a sheet as JSON. The close always ends on who else decides, the one
34
+ * beat every first call carries, set from the labels so no sheet can drop it.
35
+ */
36
+
37
+ /** A paragraph: a plain string, or runs of text and links in order. */
38
+ export type FirstCallParagraph = ArtifactParagraph;
39
+
40
+ /** One line of the brief: who, how they booked, the listing, the writeup. */
41
+ export interface FirstCallFact {
42
+ label: string;
43
+ body: FirstCallParagraph;
44
+ /** Sets the fact apart on a tinted ground, for the one that matters most. */
45
+ box?: boolean;
46
+ }
47
+
48
+ /** One numbered step of the open or the close. */
49
+ export interface FirstCallStep {
50
+ /** The step, set in bold. */
51
+ label: string;
52
+ /** The words under it, e.g. the question as it would be asked. */
53
+ text?: string;
54
+ }
55
+
56
+ /** One thing to show: the line, where it opens live, and the picture of it. */
57
+ export interface FirstCallShow {
58
+ text: string;
59
+ /** Where to open it on the call: in a new tab for an http(s) link, in place otherwise. */
60
+ href?: string;
61
+ /**
62
+ * The picture, set beside the show lines at thumbnail size and opening
63
+ * its `href` (the full capture behind a crop), or itself when there is none.
64
+ */
65
+ shot?: WriteupShot;
66
+ /** Reads as broken rather than as needing work, for the line that is plainly wrong. */
67
+ broken?: boolean;
68
+ }
69
+
70
+ /** The ids the page takes for itself (the shell's main, the sheet's sections), which no unit's id may repeat. */
71
+ export const firstCallSectionIds = ["main", "brief", "open", "close"] as const;
72
+
73
+ /** One topic: what we show, the questions it raises, the moves it leads to. */
74
+ export interface FirstCallUnit {
75
+ /** The anchor the unit chips jump to: lowercase, unique, and none of firstCallSectionIds. */
76
+ id: string;
77
+ title: string;
78
+ /** The unit in one breath, under its heading. */
79
+ tldr: string;
80
+ show: FirstCallShow[];
81
+ ask: string[];
82
+ move: string[];
83
+ }
84
+
85
+ /** The fixed words the sheet sets, each overridable through `labels`. */
86
+ export interface FirstCallLabels {
87
+ preparedFor: string;
88
+ brief: string;
89
+ open: string;
90
+ unitsNav: string;
91
+ tldr: string;
92
+ show: string;
93
+ ask: string;
94
+ move: string;
95
+ close: string;
96
+ /** The beat every close ends on. */
97
+ whoElse: string;
98
+ }
99
+
100
+ export const firstCallLabels: FirstCallLabels = {
101
+ preparedFor: "Prepared for",
102
+ brief: "The Brief",
103
+ open: "Open",
104
+ unitsNav: "Units",
105
+ tldr: "TL;DR",
106
+ show: "Show",
107
+ ask: "Ask",
108
+ move: "Move",
109
+ close: "Close",
110
+ whoElse: "Who else decides, and should they join the next call?",
111
+ };
112
+
113
+ export interface FirstCallProps {
114
+ title: string;
115
+ /** Who the call is with, set in the dateline: "Prepared for <person, business>". */
116
+ preparedFor: string;
117
+ /** The when line: the date, the 24 hour time, the place, the minutes booked. */
118
+ when: string;
119
+ brief: FirstCallFact[];
120
+ /** A picture beside the brief, e.g. the business's map card. */
121
+ briefShot?: WriteupShot;
122
+ open: FirstCallStep[];
123
+ /** One per topic, as many as the call has. */
124
+ units: FirstCallUnit[];
125
+ /** The close's steps; who else decides follows them. */
126
+ close: FirstCallStep[];
127
+ labels?: Partial<FirstCallLabels>;
128
+ }
129
+
130
+ /** The unit's number as the sheet sets it: 01, 02, and on. */
131
+ function unitNumber(index: number): string {
132
+ return String(index + 1).padStart(2, "0");
133
+ }
134
+
135
+ /** The read's numbered steps: the open and the close. */
136
+ function Steps({ steps }: { steps: FirstCallStep[] }) {
137
+ return (
138
+ <ol className="rd-steps">
139
+ {steps.map((step, i) => (
140
+ // biome-ignore lint/suspicious/noArrayIndexKey: steps are static content in a fixed order, and a label may repeat
141
+ <li className="rd-step" key={i}>
142
+ <div className="rd-step-body">
143
+ <p className="rd-step-head">
144
+ <b className="rd-step-title">{step.label}</b>
145
+ </p>
146
+ {step.text && <p>{step.text}</p>}
147
+ </div>
148
+ </li>
149
+ ))}
150
+ </ol>
151
+ );
152
+ }
153
+
154
+ /** The writeup's findings block: the callouts down the left, the pictures to the right. */
155
+ function Findings({
156
+ items,
157
+ shots,
158
+ block = false,
159
+ }: {
160
+ items: ReactNode[];
161
+ shots: WriteupShot[];
162
+ /** The block itself, as the writeup sets it, rather than inside a block under its heading. */
163
+ block?: boolean;
164
+ }) {
165
+ const classes = [block && "op-block", "op-findings", shots.length > 0 && "has-shots"];
166
+ return (
167
+ <div className={classes.filter(Boolean).join(" ")}>
168
+ <div className="op-findings-list">{items}</div>
169
+ {shots.length > 0 && (
170
+ <div className="op-shots">
171
+ {shots.map((shot, i) => (
172
+ // biome-ignore lint/suspicious/noArrayIndexKey: shots are static content in a fixed order, and a picture may repeat
173
+ <Shot shot={{ ...shot, href: shot.href ?? shot.src }} key={i} />
174
+ ))}
175
+ </div>
176
+ )}
177
+ </div>
178
+ );
179
+ }
180
+
181
+ function Unit({
182
+ unit,
183
+ index,
184
+ labels,
185
+ }: {
186
+ unit: FirstCallUnit;
187
+ index: number;
188
+ labels: FirstCallLabels;
189
+ }) {
190
+ const shows = unit.show.map((line, i) => (
191
+ <Callout
192
+ tone={line.broken ? "bad" : "warn"}
193
+ toneLabel={line.broken ? undefined : labels.show}
194
+ title={
195
+ line.href ? <FirstCallAnchor href={line.href}>{line.text}</FirstCallAnchor> : line.text
196
+ }
197
+ // biome-ignore lint/suspicious/noArrayIndexKey: show lines are static content in a fixed order
198
+ key={i}
199
+ />
200
+ ));
201
+ const shots = unit.show.flatMap((line) => (line.shot ? [line.shot] : []));
202
+ return (
203
+ <>
204
+ <div className="op-part" id={unit.id}>
205
+ <h2>
206
+ <span className="fc-unit-no">{unitNumber(index)}</span> {unit.title}
207
+ </h2>
208
+ </div>
209
+ <section className="op-topic">
210
+ <header className="op-topic-head">
211
+ <div>
212
+ <h3>{labels.tldr}</h3>
213
+ <p className="op-def">{unit.tldr}</p>
214
+ </div>
215
+ </header>
216
+ <div className="op-blocks">
217
+ {shows.length > 0 && (
218
+ <div className="op-block rd-beat fc-show">
219
+ <h4>{labels.show}</h4>
220
+ <Findings items={shows} shots={shots} />
221
+ </div>
222
+ )}
223
+ {unit.ask.length > 0 && (
224
+ <div className="op-block rd-beat fc-ask">
225
+ <h4>{labels.ask}</h4>
226
+ <IconList
227
+ icon={<MessageCircleQuestion size={16} />}
228
+ items={unit.ask.map((text) => ({ body: text }))}
229
+ />
230
+ </div>
231
+ )}
232
+ {unit.move.length > 0 && (
233
+ <div className="op-block rd-beat fc-move">
234
+ <h4>{labels.move}</h4>
235
+ <IconList
236
+ icon={<ArrowRight size={16} strokeWidth={2.5} />}
237
+ items={unit.move.map((text) => ({ body: text }))}
238
+ />
239
+ </div>
240
+ )}
241
+ </div>
242
+ </section>
243
+ </>
244
+ );
245
+ }
246
+
247
+ export function FirstCall({
248
+ title,
249
+ preparedFor,
250
+ when,
251
+ brief,
252
+ briefShot,
253
+ open,
254
+ units,
255
+ close,
256
+ labels: overrides,
257
+ }: FirstCallProps) {
258
+ const labels = { ...firstCallLabels, ...overrides };
259
+ const facts = brief.map((fact, i) => (
260
+ <Callout
261
+ tone="info"
262
+ variant={fact.box ? "box" : "plain"}
263
+ title={fact.label}
264
+ // biome-ignore lint/suspicious/noArrayIndexKey: the brief is static content in a fixed order
265
+ key={i}
266
+ >
267
+ <Runs paragraph={fact.body} />
268
+ </Callout>
269
+ ));
270
+ return (
271
+ <div className="op rd fc">
272
+ <article className="op-doc rd-doc">
273
+ <header className="op-head">
274
+ <p className="op-meta">
275
+ {labels.preparedFor} <b>{preparedFor}</b>
276
+ <span className="op-meta-sep" aria-hidden="true">
277
+ {" · "}
278
+ </span>
279
+ <span className="op-meta-date">{when}</span>
280
+ </p>
281
+ <h1>{title}</h1>
282
+ </header>
283
+
284
+ {facts.length > 0 && (
285
+ <section className="op-topic" id="brief">
286
+ <header className="op-topic-head">
287
+ <div>
288
+ <h3>{labels.brief}</h3>
289
+ </div>
290
+ </header>
291
+ <div className="op-blocks">
292
+ <Findings items={facts} shots={briefShot ? [briefShot] : []} block />
293
+ </div>
294
+ </section>
295
+ )}
296
+
297
+ {open.length > 0 && (
298
+ <section className="rd-plan" id="open">
299
+ <h2>{labels.open}</h2>
300
+ <Steps steps={open} />
301
+ </section>
302
+ )}
303
+ {/* Outside the read's plan section, whose link rule would restyle every chip as a prose link. */}
304
+ {units.length > 1 && (
305
+ <nav className="fc-units" aria-label={labels.unitsNav}>
306
+ {units.map((unit, i) => (
307
+ <Chip dot href={`#${unit.id}`} key={unit.id}>
308
+ {i + 1}. {unit.title}
309
+ </Chip>
310
+ ))}
311
+ </nav>
312
+ )}
313
+
314
+ {units.map((unit, i) => (
315
+ <Unit unit={unit} index={i} labels={labels} key={unit.id} />
316
+ ))}
317
+
318
+ <section className="rd-plan" id="close">
319
+ <h2>{labels.close}</h2>
320
+ <Steps steps={[...close, { label: labels.whoElse }]} />
321
+ </section>
322
+ </article>
323
+ </div>
324
+ );
325
+ }
@@ -1,10 +1,10 @@
1
1
  /* The composed documents in this package: the sealed document stage (Gate),
2
- the monthly page (Report), the writeup (Writeup), the note (Note), and the
3
- read (Read). Moved from @atelic-action/ui's styles/artifact.css with the
2
+ the monthly page (Report), the writeup (Writeup), the note (Note), the
3
+ read (Read), and the first call sheet (FirstCall). Moved from @atelic-action/ui's styles/artifact.css with the
4
4
  documents (ATE-671), in the order that sheet bundled them, which the two
5
5
  @page rules depend on: the later one wins, and the writeup's is the later
6
- one. The note and the read follow the writeup and set no @page, so the
7
- writeup's stays the one that wins.
6
+ one. The note, the read, and the first call sheet follow the writeup and
7
+ set no @page, so the 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
10
  (.op-grade), the sign off (.op-who, .op-sign), the shot (.op-shot), the
@@ -2385,4 +2385,41 @@
2385
2385
  break-after: avoid;
2386
2386
  }
2387
2387
  }
2388
+
2389
+ /* ---- the first call sheet (FirstCall): the writeup's topic and findings
2390
+ blocks and the read's steps carry it; these are the few rules it adds
2391
+ (the Claude Design cut of 2026-10-23) ---- */
2392
+ .atelic-ui .fc-units {
2393
+ display: flex;
2394
+ flex-wrap: wrap;
2395
+ gap: 8px;
2396
+ margin-top: 18px;
2397
+ }
2398
+ /* A long unit title wraps rather than pushing the phone page sideways. */
2399
+ .atelic-ui .fc-units .chip {
2400
+ white-space: normal;
2401
+ }
2402
+ .atelic-ui .fc-unit-no {
2403
+ font-family: var(--font-mono);
2404
+ font-weight: 500;
2405
+ color: var(--primary);
2406
+ margin-right: 6px;
2407
+ }
2408
+ .atelic-ui .fc .fc-ask .icon-list {
2409
+ --tone: var(--ink);
2410
+ }
2411
+ .atelic-ui .fc .fc-move .icon-list {
2412
+ --tone: var(--primary);
2413
+ }
2414
+ /* A show line links to where it opens live, but reads as a plain callout:
2415
+ the link takes the ink and shows itself on hover. */
2416
+ .atelic-ui .fc-show .callout-title a {
2417
+ color: inherit;
2418
+ text-decoration: none;
2419
+ }
2420
+ .atelic-ui .fc-show .callout-title a:hover,
2421
+ .atelic-ui .fc-show .callout-title a:focus-visible {
2422
+ color: var(--primary);
2423
+ text-decoration: underline;
2424
+ }
2388
2425
  }
@@ -4,6 +4,7 @@
4
4
  * chip, the sign off, the page components, the button), which this package
5
5
  * depends on and never copies, and wears the styles in ./documents.css.
6
6
  */
7
+ export * from "./FirstCall";
7
8
  export { Gate, type GatePayload, type GatePerson, type GateProps, normalizeEmail } from "./Gate";
8
9
  export * from "./Note";
9
10
  export * from "./Read";