@atelic-action/artifact
v6.6.0
Published
The composed documents the Atelic practice hands or sends a business (the read, the monthly page, the note, the discovery sheet, the first call sheet, and the sealed document), built from the pieces in @atelic-action/ui
Readme
@atelic-action/artifact
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:
- The package,
@atelic-action/artifacton npm: the composed documents (the read, the monthly page, the note, the discovery sheet, the first call sheet, and the sealed document), each with its prop types and its default labels, and the stylesheet they wear. It shipssrc/documents/and nothing else, as source, the way@atelic-action/uidoes, and depends on that package for every piece a document is built from. - 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/artifactsis cut from, and the template site that shows every document on fictional content.
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.
Six artifacts live in the template site: the read at /the-read, the graded document a business gets from the practice, built from one snapshot at one of two depths (a prospect's, from public readings alone; a customer's, from their own consoles as well), a web page first with its printed copy derived from it, the grades explained by cause, under a standard head (the dateline, the sources, the title and the lede the read sets from the business), with the situation, the plan, and the proposal each in a tab of its own ahead of the full read; the proposal at /proposal, the sealed page that stays for the proposals sent before the tab existed, 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; the discovery sheet at /discovery, the desk's page for the first conversation with a business across a table, used to guide the call and not sent for now, written so it could be shared with them later; and the first call sheet at /first-call, the desk's page for a first call by phone, one numbered run of single questions with the practice's give in the middle, which carries desk notes and so sits behind the sign in. The bare host sends a reader to the read. Every artifact prerenders to static HTML, stays out of every index, and carries the practice's brand by default.
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.
The example inside is Summit Movement Studio, a fictional physical therapy practice with invented numbers that exercise the components. Replace it; never ship it.
This repo was cut from template-marketing on 2026-09-09 with its history kept, so it shares the stack below. The two split apart on 2026-09-10: each template now owns its own files and nothing merges between them. It was named template-artifact until 2026-10-15, when it became the package's home as well; GitHub redirects the old name.
Component Index
Every export of the package, all from the one entry, @atelic-action/artifact. The route column is the template page that shows it on fictional content.
| Export | What it is | Route |
|---|---|---|
| TheRead, TheReadProps | The graded document a business gets, built from one snapshot: the dated lede; the scorecard, a ReportCard with each row a link to its card and the earlier letter beside a row that moved, with an optional note callout over it for what the read leaves out; the plan's numbered steps between the scorecard and the cards when plan has any (superseded by the plan tab, and removed at the next major); then the tiers, each section on the card (the trade name linked to its definition, the one liner, the grade, the why line as the summary, the blocks, and the sources at the foot) plus the read's own today and after (a two column table) and what we recommend (a checklist), a tier opening on a markets table of letters and closing on the rented line when it has them; the closing line and the sign off. access says which depth it is ("public", from public readings alone, or "private", from the owner's consoles too), and a public read with no note gets the publicNote label over its scorecard. print adds the lockup and the twin's QR to the printed copy. A beat with no data is left out. The head is the same for every business: title left out is the overviewTitle label and preparedFor ("Overview of Summit Movement Studio"), lede left out is the overviewLede label with the business in it, and under the lede the fine print, a row each with its word on the left: period ("2026-08-01 to 2026-10-31", the window every reading on the page is read over, under the periodLabel word) and sources (the public sources on one line, the private ones on the next, each a link in a new tab, under the sourcesLabel word); a read that sets neither has no fine print. The page is up to four tabs under that head, in a fixed order, each document tab present only when its document is: given a stateDocument, The Situation (the problems, by tier, each linked to its card); given a planDocument, The Plan; given a proposalDocument, The Proposal; and The Full Read, which holds everything that sat between the head and the sign off, always last. The first tab present opens. The sign off stays under all of them, and the tab row's screen reader name, unless labels sets one, names the tabs present. The tabs are ui's Tabs, so a link or a hash naming a card in the read opens the read tab and lands on it, and on paper every panel prints in order, each after the first on a new page under its tab's name. selectedTab and onSelectTab let a route hold the open tab, for instance to swap the header's stops with it. With no document the page is the one it always was | /the-read |
| readLabels, ReadLabels | The fixed words the read sets (Prepared for, the scorecard's screen reader words, Before, Rented, Today, After, Recommendations, the plan's heading, the public note, the standard head's words (overviewTitle, overviewLede with {business} in it, periodLabel, sourcesLabel, publicSources, privateSources), the four tabs' names (stateTab, planTab, proposalTab, readTab: The Situation, The Plan, The Proposal, The Full Read), and the tab row's name for a screen reader, which names the tabs present when left out), each overridable through labels | /the-read |
| StateDocument | The situation tab's document, the same shape as PlanDocument: the problems the read found, one section per tier (Health, Visibility, Conversion), each line linked to the card that shows it, so the plan answers them in the same order. Its panel wears rd-state-doc beside rd-plan-doc. Its id defaults to theStateTabId and its title to the stateTab label | /the-read |
| ReadSources | The sources the read was built from, for the fine print's sources row: public links (the site, Google, PageSpeed, the AI answers) and the owner's private consoles | /the-read |
| PlanDocument, PlanSection, PlanPart, PlanBlock | The shapes the plan tab is written in, all plain data, so a routine can write a plan as JSON. A plan is an optional lede and its sections; a section is an id (the anchor a header stop points at), a heading, an optional timeline line set small under the heading (when the work happens and whose it is), its blocks in order, and any parts, each a smaller heading with the same body. A block is a paragraph, a bulleted or numbered list, an icon list (each item led by an icon named from planIcons, PlanIcon, PlanIconItem, so a problem on the situation and its fix on the plan wear the same one), or a table with an optional head row, and every piece of text in one is a paragraph, so a link into the read is a run whose href is a card's anchor. A block with nothing in it is left out. On a phone a table's rows stack, each cell wearing its column's name | /the-read |
| ProposalDocument | The proposal tab's document, the same shape as PlanDocument: what the owner gets, what it costs, the owner's own questions, and the next steps, with a line that rests on a finding linking to its card (What You Get, What It Costs, Your Questions, and Next Steps in the example). Its panel wears rd-proposal-doc beside rd-plan-doc, so the plan's styles dress it unchanged. Its id defaults to theProposalTabId and its title to the proposalTab label | /the-read |
| theStateTabId, thePlanTabId, theProposalTabId, theReadTabId | The four tabs' ids ("state", "plan", and "proposal", unless the document names its own, and "read"), so a route compares the open tab without a string of its own. No anchor on the page may share any of them | none |
| ReadAccess, ReadTier, ReadSection, ReadChange, ReadRecommendation, ReadMarkets, ReadMarketRow, ReadRented, ReadStep, ReadPlan (both superseded by the plan tab), 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 |
| 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 |
| ratingCell | A star rating table cell for a table block, sorted by its figure | /the-read |
| 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 |
| 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 |
| Report, ReportProps | The monthly page: the four questions, the seven grades now and before, the sign off, on one letter sheet | /report |
| ReportLead, ReportLeads, ReportGradeRow | The shapes a monthly page's content is written in | none |
| Note, NoteProps | The first touch set as a page: the opener, two lanes of findings, where the work would start, the close | /notes |
| NoteLane, NoteFinding, NoteStart, NoteStep, NoteClose, NoteLink, NoteParagraph | The shapes a note's content is written in | none |
| Discovery, DiscoveryProps | The desk's sheet for the first conversation with a business: 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 read | /discovery |
| discoveryLabels, DiscoveryLabels | The fixed words the sheet sets (Show, Ask, Move, the close's who else decides beat, and the rest), each overridable through labels | /discovery |
| discoverySectionIds | 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 |
| DiscoveryFact, DiscoveryStep, DiscoveryShow, DiscoveryUnit, DiscoveryParagraph | The shapes a discovery sheet's content is written in, all plain data, so a skill writes the sheet as JSON | none |
| FirstCall, FirstCallProps | The desk's sheet for a first call by phone: the brief as labeled lines, the call as one numbered run of single questions in blocks, what the practice says (the give) as iconed lines with a fix under each, a note under a question saying what the answer means, and the qualifying checks as chips at the foot. The desk's page alone, never sent, so it carries desk posture and sits behind the sign in | /first-call |
| firstCallLabels, FirstCallLabels | The fixed words the sheet sets (Prepared for, Brief, Say, Qualify), each overridable through labels | /first-call |
| FirstCallFact, FirstCallBlock, FirstCallQuestion, FirstCallSay, FirstCallIcon, FirstCallParagraph | The shapes a first call sheet's content is written in, all plain data, so a skill writes the sheet as JSON | none |
| Gate, GateProps, GatePayload, GatePerson | The sealed document, opened in the browser by the reader's own address, and the payload it opens; the proposals sent before the read's proposal tab existed live here | /proposal |
| normalizeEmail | How a typed address becomes the key a payload opens with; the sealing command mirrors it | none |
| documents.css | The documents' styles, at @atelic-action/artifact/styles/documents.css | every route |
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, Tabs, 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.
Install
bun add @atelic-action/artifact @atelic-action/uiPeer dependencies: @atelic-action/ui at ^3.10.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.
import { TheRead } from "@atelic-action/artifact";
import { buildPageHead } from "@atelic-action/ui/artifact";CSS Import Order
The documents' sheet goes after every ui sheet and before the site's theme.css:
- The site's fonts (
fonts.css) @atelic-action/ui/styles/base.css,chrome.css,components.css,primitives.css, andgate.css, in that order@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@atelic-action/artifact/styles/documents.css- The site's
theme.css, last
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 read's, and the later one wins for every page a site prints; their order is load bearing.
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.
Tech Stack
| Category | Technology | Purpose |
|----------|------------|---------|
| Runtime | Bun | Fast JS runtime with built in tooling |
| Framework | React 19 + TypeScript | Strict mode throughout |
| Meta framework | TanStack Start | File based routing with static prerendering (SSG) |
| Styling | Semantic CSS tokens | One theme file per artifact; component CSS on top; no utility framework |
| Confidentiality | Web Crypto | The sealed document gate, plus an optional email tied wall over the whole host |
| Error tracking | Sentry | Optional, activates only when a DSN is set |
| Testing | Vitest + Testing Library | Tests in tst/ mirroring src/ |
| Lint and format | Biome | Pre commit hook lints; pre push mirrors CI |
Quick Start
bun install
bun devNo environment variables required. Visit http://localhost:5173.
Building a Client Artifact
A business's artifacts live in one folder of atelic-action/artifacts, 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, 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 artifacts. 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, 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:
- 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'sFOOTPRINT.mdand written to its pitch surface rules (Brand/VOICE.md, Pitch Surfaces 15 to 21). Inthe-read.tsx,accesssays whether any reading came from the owner's own consoles, which picks the caveat over the scorecard and says nothing about the host; print flows the page across as many letter pages as it needs, andtheReadStopsnames the header's stops. - 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.pngwith theqrcodepackage), go inpublic/images/, which the wall holds on a private artifact. A cut made before this split moves its recipient logo frompublic/images/topublic/brand/and repointsrecipient.logo.src. - Config, in
src/site.config.ts.urlis the host,accessis always"private", since every document sits behind the wall (the depth of the readings is the read's ownaccess, not this one),recipientis the business it was prepared for (with itslogounder/brand/),senderis the practice, andanalyticsis the HubSpot loader so an open reports to the CRM, withidentifynaming who each link identifies so the open lands on that person's contact (bun run reader <email>mints a reader, sealed so no address ships in the page's script, and prints their link; a private artifact needs nobody listed, because the wall identifies whoever signs in; DEPLOY.md, Who Opened It). - The printed copy:
bun build, thenbun run print dist/client public/the-read.pdf, which prints with every collapsible open and reports the sheet count, orbun run print dist/client <out.pdf> 1 /reportfor the monthly page, which fails if it misses its one sheet. Commit the read's PDF beside the page, and file a dated copy in the client's folder of the practice repo.
Every artifact deploys only behind the armed wall, which holds the screenshots, the PDF, and the built content chunks as well as the pages, and bun run check:wall <short>.atelic.me must pass before any link goes out (ACCESS-GATE.md).
The theme stays the practice's (Geist, ink, the orange) unless the artifact should wear the client's brand, in which case src/styles/theme.css and the two font files swap and nothing else moves. Check the phone at 390 wide in a real browser before deploying.
Documentation Map
| Doc | Covers | |-----|--------| | CLAUDE.md | Architecture, conventions, and gotchas (agents start here) | | src/components/README.md | The component catalog: every component, its props, where it's used | | src/components/CLAUDE.md | Rules for changing or growing the components | | src/content/README.md | Authoring the content modules | | src/styles/README.md | The theming system and how to derive a new palette | | GATE.md | The sealed document gate: opens with the reader's own email, with HubSpot open tracking | | ACCESS-GATE.md | The email tied sign in wall over the whole host, always on, with the build guard and the wall check | | DEPLOY.md | The deploy motion: Vercel, the subdomain, the wall for a private artifact, the observability layers | | LAUNCH.md | The per artifact send checklist template, stamped into a Linear ticket | | TEARDOWN.md | The reverse motion: retiring a subdomain and its Vercel project |
Scripts
| Command | Description |
|---------|-------------|
| bun dev | Dev server |
| bun build | Access guard + typecheck + build + prerender every page |
| bun test / bun test:run | Tests (watch / once) |
| bun lint / bun lint:fix | Biome check / auto fix |
| bun run print <url or dist/client> <out.pdf> [sheets] [page] | Prints a page (the read unless named) through headless Chrome, reports the sheet count, and checks it when one is given |
| bun run check:wall <host> [path ...] | Proves a private artifact's live host answers every document with the wall |
| bun run seal <config.json> [out.json] | Seals a document for its named readers (GATE.md) |
Deployment
Static files. vercel.json points Vercel at dist/client with no framework preset. The full motion, the subdomain, the observability layers, and why every .atelic.me host is noindexed automatically live in DEPLOY.md.
Contributing Back
A document is fixed or grown here, in src/documents/, with its test in tst/documents/, and released as a version of this package; a folder takes it with bun update @atelic-action/artifact. A piece a document is built from (the grade chip, the sign off, a page component, the chrome, the wall, a command) is fixed in @atelic-action/ui and released there first, then taken here by bumping the ui range. An improvement to the seed itself (a route, a stub file, the example content) is made here and copied into the folders by hand.
A folder taking 3.0.0 moves three things in its own files, since 3.0.0 draws the read's scorecard and the writeup's overview as ui's ReportCard and removed the old markup with no alias. A table cell its content wrote as <span className="rd-ranked"> becomes ui's Rank (figure, tone, meaning, href). A rule in its own sheets aimed at .rd-gist-table, .rd-gist-card, .rd-gist-grade, .rd-gist-read, .rd-gist-section, .rd-gist-tier, .op-legend, or any .op-ov- class matches nothing now, so it is rewritten against the card's .rc- classes or dropped. And a read's labels loses its section key, which the table's first column wore and the card has no use for.
A folder taking 4.0.0 takes ui 3.0.0 with it (bun update @atelic-action/artifact @atelic-action/ui --latest --cwd <short>) and repoints its own routes, since ui 3.0.0 keeps acronyms capitalized with no alias for the old spelling: canonicalUrl from @atelic-action/ui/artifact becomes canonicalURL in src/routes/index.tsx, and the root route's baseCss import becomes baseCSS.
Releasing
Bump
versioninpackage.jsonand merge it tomain: patch for a fix, minor for a new document or prop, major for a breaking prop, type, or class rename.Tag the merged commit
vX.Y.Zwith the same version and push the tag:git tag v1.0.0 git push origin v1.0.0The Publish workflow (
.github/workflows/publish.yml) checks that the tag matches the version, runs lint, the tests, and the build, and publishes to npm. It authenticates through npm trusted publishing, as ui's does: the package's Trusted Publisher names this repo andpublish.yml, and its allowed actions must permit directnpm publish, since a configuration created after 2026-09-03 allows onlynpm stage publishby default. No token is stored, and it needs npm 11.5.1 or later, which Node 24 ships.
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.
License
MIT
