@atelic-action/ui 3.3.0 → 3.4.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
@@ -23,6 +23,7 @@ Every export, grouped by the entry it is imported from. The gallery column links
23
23
  | `IconList` | `@atelic-action/ui/components` | A list whose bullets are icons, one for the list or one per item, each in a tone | [#icon-list](https://ui.atelic.me/#icon-list) |
24
24
  | `DataTable` | `@atelic-action/ui/components` | A table whose first column names each row, sortable, stacking on a phone | [#data-table](https://ui.atelic.me/#data-table) |
25
25
  | `ReportCard` | `@atelic-action/ui/components` | A name, a grade, and a line per row, in labeled groups, each row a link to its section | [#report-card](https://ui.atelic.me/#report-card) |
26
+ | `reportCardLabels`, `ReportCardLabels` | `@atelic-action/ui/components` | The words a screen reader hears around a report card's letters by default, and their type | none |
26
27
  | `Rank` | `@atelic-action/ui/components` | A figure with its badge in a tone, for a table cell: a rank, a place in an answer, or a dash | [#rank](https://ui.atelic.me/#rank) |
27
28
  | `StatRow` | `@atelic-action/ui/components` | A row of large figures with periods, deltas, and trends | [#stat-row](https://ui.atelic.me/#stat-row) |
28
29
  | `Callout` | `@atelic-action/ui/components` | One finding: an icon in its tone, the title, the explainer | [#callout](https://ui.atelic.me/#callout) |
@@ -365,7 +366,7 @@ The pieces import no stylesheet of their own. A site imports `@atelic-action/ui/
365
366
  // vite.config.ts
366
367
  import { artifactConfig } from "@atelic-action/ui/vite";
367
368
 
368
- export default artifactConfig({ pages: ["/writeup", "/proposal", "/report"] });
369
+ export default artifactConfig({ pages: ["/the-read", "/proposal", "/report"] });
369
370
  ```
370
371
 
371
372
  `artifactConfig` returns the TanStack Start plugin with prerendering on, the crawler following links but kept out of `/images/` (where a prerendered page would overwrite a real file), one prerendered page for `/`, for each path the site names, and for `/404`, and no sitemap; the React plugin; `privateChunks()`; the `@` alias to the site's `src`, resolved from the directory Vite is run in; and `ssr.noExternal: ["@atelic-action/ui", "@atelic-action/artifact"]`, so the documents compile too. Nothing links to an artifact page, so a page a site leaves out of `pages` ships with no HTML in it. `artifactPages` is the list alone.
@@ -374,7 +375,7 @@ A site with a real need of its own passes `extend`, which is merged over the res
374
375
 
375
376
  ```ts
376
377
  export default artifactConfig({
377
- pages: ["/writeup"],
378
+ pages: ["/the-read"],
378
379
  extend: { server: { port: 5180 } },
379
380
  });
380
381
  ```
@@ -397,7 +398,7 @@ The package installs the scripts every artifact site used to carry as files, so
397
398
  | `atelic-stamp-health` | Writes `dist/client/health.json` with the status, the commit, and the build time | Last step of `build` |
398
399
  | `atelic-check-links` | Asks for every external link in the prerendered pages and exits 1 when one is dead | `check:links`, after a build |
399
400
  | `atelic-check-wall <host> [path ...]` | Asks a deployed host for each document and held file with no cookie, and passes only when the wall answers every one and the sign in functions respond | `check:wall`, before any private link goes out |
400
- | `atelic-reader-link <email> [path] [--token <token>] [--qr \| --no-qr]` | Mints a named reader: prints the sealed line for `analytics.identify` and the link to send, and writes `public/images/qr.png` for a public writeup's first reader | `reader` |
401
+ | `atelic-reader-link <email> [path] [--token <token>] [--qr \| --no-qr]` | Mints a named reader: prints the sealed line for `analytics.identify` and the link to send, and writes `public/images/qr.png` for a public read's first reader | `reader` |
401
402
  | `atelic-serve-static` | Serves `dist/client` on `127.0.0.1` the way Vercel does, on `PORT` or 4173 | The `webServer` command in `playwright.config.ts` |
402
403
  | `atelic-print-sheets <url or dist/client> <out.pdf> [sheets] [page] [port]` | Prints a page to PDF with headless Chrome, reports the sheet count, and fails when a count is given and missed | `print` |
403
404
  | `atelic-seal-document <config.json> [out.json]` | Seals a document into the payload that `@atelic-action/artifact`'s `Gate` reads, one wrapped key for each reader. The payload names no one: no address, and readers numbered `r1`, `r2` whatever the config calls them | `seal` |
@@ -1,21 +1,21 @@
1
1
  #!/usr/bin/env bash
2
2
  # Print an artifact page to PDF with headless Chrome and report its sheet
3
- # count. The writeup flows across as many letter pages as it needs, its
3
+ # count. The read flows across as many letter pages as it needs, its
4
4
  # native disclosures printed open, so its count is a record of the print and not a
5
5
  # layout check. The monthly page is one sheet by construction, so pass 1 for
6
6
  # it and the script fails when it prints as anything else.
7
7
  #
8
8
  # Usage: atelic-print-sheets <url | dist/client> <out.pdf> [expected sheets] [page] [port]
9
- # atelic-print-sheets dist/client public/writeup.pdf
9
+ # atelic-print-sheets dist/client public/the-read.pdf
10
10
  # atelic-print-sheets dist/client ../2026-10-01-report.pdf 1 /report
11
- # atelic-print-sheets https://summit.atelic.me/writeup ../2026-09-10-writeup.pdf
11
+ # atelic-print-sheets https://summit.atelic.me/the-read ../2026-09-10-the-read.pdf
12
12
  # A site offers the same as `bun run print <...>`. Both paths are read from
13
13
  # the directory the command is run in, which is the site's root.
14
14
  #
15
15
  # A directory is served on a local port for the print (the page loads its
16
16
  # fonts and images by absolute path) and the server is stopped after; the
17
- # page is the path printed from it, /writeup unless named. A URL is printed
18
- # as given. The writeup's PDF belongs in public/ as writeup.pdf beside the
17
+ # page is the path printed from it, /the-read unless named. A URL is printed
18
+ # as given. The read's PDF belongs in public/ as the-read.pdf beside the
19
19
  # page (on a private artifact the wall holds it like the page, see
20
20
  # ACCESS-GATE.md), and a dated copy goes in the client folder of the
21
21
  # practice repo.
@@ -24,7 +24,7 @@ set -euo pipefail
24
24
  TARGET=${1:?url or built directory required}
25
25
  OUT=${2:?output pdf path required}
26
26
  EXPECT=${3:-}
27
- PAGE=${4:-/writeup}
27
+ PAGE=${4:-/the-read}
28
28
  PORT=${5:-8765}
29
29
  CHROME=${CHROME:-"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"}
30
30
  [[ -x "$CHROME" ]] || { echo "Chrome not found at $CHROME" >&2; exit 1; }
@@ -110,7 +110,7 @@ export async function whoIsReading(visit) {
110
110
  return openReader(visit.readers, token);
111
111
  }
112
112
  /** The link to send a reader: the artifact's page with their token on it. */
113
- export function readerLink(siteURL, token, path = "/writeup") {
113
+ export function readerLink(siteURL, token, path = "/the-read") {
114
114
  const url = new URL(path, siteURL);
115
115
  url.searchParams.set("k", token);
116
116
  return url.toString();
@@ -1,7 +1,7 @@
1
1
  import { type UserConfig } from "vite";
2
2
  export interface ArtifactConfigOptions {
3
3
  /**
4
- * The path of each artifact page the site carries, such as "/writeup".
4
+ * The path of each artifact page the site carries, such as "/the-read".
5
5
  * The root and the 404 page are always prerendered and need no entry.
6
6
  */
7
7
  pages: readonly string[];
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * import { artifactConfig } from "@atelic-action/ui/vite";
7
7
  *
8
- * export default artifactConfig({ pages: ["/writeup", "/proposal", "/report"] });
8
+ * export default artifactConfig({ pages: ["/the-read", "/proposal", "/report"] });
9
9
  *
10
10
  * The site's own paths (the `@` alias to its src) are found from the
11
11
  * directory Vite is run in, which is the site's root.
@@ -19,7 +19,7 @@ import { privateChunks } from "./private-chunks.js";
19
19
  export function artifactPages(pages) {
20
20
  const named = pages.filter((path) => path !== "/" && path !== "/404");
21
21
  return [
22
- // The root holds no document: it sends the reader to /writeup
22
+ // The root holds no document: it sends the reader to /the-read
23
23
  // (vercel.json does the same on the host), and its prerendered
24
24
  // index.html is what a static preview serves at the bare host.
25
25
  { path: "/", prerender: { enabled: true } },
@@ -44,10 +44,10 @@ export function artifactConfig({ pages, extend }) {
44
44
  prerender: {
45
45
  enabled: true,
46
46
  crawlLinks: true,
47
- // The writeup links each screenshot crop to its full image, and
47
+ // The read links each screenshot crop to its full image, and
48
48
  // the crawler follows every href it finds: without this filter it
49
49
  // prerenders /images/<shot>.png as an HTML page and writes that
50
- // over the real file in dist/client (found on a client's writeup,
50
+ // over the real file in dist/client (found on a client's read,
51
51
  // 2026-09-24).
52
52
  filter: ({ path }) => !path.startsWith("/images/"),
53
53
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atelic-action/ui",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "description": "Shared UI for the Atelic templates: site chrome, base styles, the component library, the access gate, and the app shell they install",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -139,7 +139,7 @@ export async function whoIsReading(visit: {
139
139
  }
140
140
 
141
141
  /** The link to send a reader: the artifact's page with their token on it. */
142
- export function readerLink(siteURL: string, token: string, path = "/writeup"): string {
142
+ export function readerLink(siteURL: string, token: string, path = "/the-read"): string {
143
143
  const url = new URL(path, siteURL);
144
144
  url.searchParams.set("k", token);
145
145
  return url.toString();
@@ -6,9 +6,9 @@
6
6
  * readable by anyone holding the link, and a 5xx is a host that is down
7
7
  * rather than walled; both fail.
8
8
  *
9
- * The documents are /writeup, plus /report and /proposal when this repo
9
+ * The documents are /the-read, plus /report and /proposal when this repo
10
10
  * carries those routes. The wall holds files too (needsSession in @atelic-action/ui/gate), so it
11
- * also asks for the printed copy at /writeup.pdf, the first real screenshot
11
+ * also asks for the printed copy at /the-read.pdf, the first real screenshot
12
12
  * under public/images/, and, when the repo has been built, the first chunk
13
13
  * under /assets/doc/, where a document's content compiles to. Each of those
14
14
  * must answer with the same redirect: the wall holds a path whether or not
@@ -30,7 +30,7 @@
30
30
  * Usage: atelic-check-wall <host> [path ...], or `bun run check:wall <host>`
31
31
  * in a site, from the site's root
32
32
  * bun run check:wall summit.atelic.me
33
- * bun run check:wall https://summit.atelic.me /writeup /report
33
+ * bun run check:wall https://summit.atelic.me /the-read /report
34
34
  * The sign in functions are asked only when no path is named.
35
35
  * Exits 1 unless every path answers as it must.
36
36
  */
@@ -93,10 +93,10 @@ export interface WallTargets {
93
93
 
94
94
  /** The paths to ask for: the documents this repo carries, the held files, then the open mark. */
95
95
  export function wallProbes({ hasRoute, image, docChunk, brand }: WallTargets): Probe[] {
96
- const probes: Probe[] = [{ path: "/writeup", kind: "document" }];
96
+ const probes: Probe[] = [{ path: "/the-read", kind: "document" }];
97
97
  if (hasRoute("report")) probes.push({ path: "/report", kind: "document" });
98
98
  if (hasRoute("proposal")) probes.push({ path: "/proposal", kind: "document" });
99
- probes.push({ path: "/writeup.pdf", kind: "file" });
99
+ probes.push({ path: "/the-read.pdf", kind: "file" });
100
100
  if (image) probes.push({ path: image, kind: "file" });
101
101
  if (docChunk) probes.push({ path: docChunk, kind: "file" });
102
102
  if (brand) probes.push({ path: brand, kind: "open" });
@@ -8,7 +8,7 @@
8
8
  * email encrypted under it. The token itself appears only in the link, so
9
9
  * keep the link; it cannot be printed again.
10
10
  *
11
- * A writeup's reader also gets the printed copy's QR: public/images/qr.png
11
+ * A read's reader also gets the printed copy's QR: public/images/qr.png
12
12
  * is written to carry the same link, so a scan of the sheet lands on that
13
13
  * person's contact as an emailed open does. Mint the reader before printing.
14
14
  *
@@ -18,7 +18,7 @@
18
18
  *
19
19
  * Usage: atelic-reader-link <email> [path] [--token <existing token>] [--qr | --no-qr],
20
20
  * or `bun run reader <email>` in a site, from the site's root.
21
- * path defaults to /writeup. --token seals an email under a token that is
21
+ * path defaults to /the-read. --token seals an email under a token that is
22
22
  * already in a sent link, so that link keeps working. --qr writes the QR
23
23
  * for this reader even when another is already named; --no-qr never writes it.
24
24
  */
@@ -51,8 +51,8 @@ export function parseReaderArgs(flags: string[]): ReaderArgs | undefined {
51
51
  const at = args.indexOf("--token");
52
52
  const given = at === -1 ? undefined : args[at + 1];
53
53
  const rest = at === -1 ? args : args.filter((_, index) => index !== at && index !== at + 1);
54
- const [email, givenPath = "/writeup"] = rest;
55
- // "writeup", "/writeup", and "/writeup/" are one page.
54
+ const [email, givenPath = "/the-read"] = rest;
55
+ // "the-read", "/the-read", and "/the-read/" are one page.
56
56
  const path = `/${givenPath.replace(/^\/+|\/+$/g, "")}`;
57
57
 
58
58
  if (!email?.includes("@") || (at !== -1 && !given)) return undefined;
@@ -91,13 +91,13 @@ export async function main(flags: string[], cwd: string = process.cwd()): Promis
91
91
  // in and never by a token, so its QR stays on the bare host.
92
92
  const named = site.analytics?.identify.length ?? 0;
93
93
  const sheetIsFree = named === 0 || forceQR;
94
- if (path === "/writeup" && !noQR && site.access === "public" && !sheetIsFree) {
94
+ if (path === "/the-read" && !noQR && site.access === "public" && !sheetIsFree) {
95
95
  console.log("");
96
96
  console.log(
97
97
  "Left public/images/qr.png alone: a reader is already named, and the sheet is theirs. Pass --qr to give the sheet to this reader.",
98
98
  );
99
99
  }
100
- if (path === "/writeup" && !noQR && site.access === "public" && sheetIsFree) {
100
+ if (path === "/the-read" && !noQR && site.access === "public" && sheetIsFree) {
101
101
  const qr = resolve(cwd, "public/images/qr.png");
102
102
  await QRCode.toFile(qr, link, { width: 330, margin: 4, errorCorrectionLevel: "M" });
103
103
  console.log("");
@@ -661,11 +661,23 @@
661
661
  grid-column: 4;
662
662
  }
663
663
  }
664
+ /* A clipping card can defeat a row's break-inside in print engines other
665
+ than Chrome, so the card stops clipping there and the first group
666
+ label rounds its own top corners instead. A label or a heading never
667
+ ends a page alone. */
664
668
  @media print {
665
669
  .atelic-ui .rc-card {
670
+ overflow: visible;
666
671
  box-shadow: none;
667
672
  border-color: var(--neutral-200);
668
673
  }
674
+ .atelic-ui .rc-card > .rc-group:first-child {
675
+ border-radius: calc(var(--radius-md) - 1px) calc(var(--radius-md) - 1px) 0 0;
676
+ }
677
+ .atelic-ui .rc-head,
678
+ .atelic-ui .rc-group {
679
+ break-after: avoid;
680
+ }
669
681
  .atelic-ui .rc-row {
670
682
  break-inside: avoid;
671
683
  }
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * import { artifactConfig } from "@atelic-action/ui/vite";
7
7
  *
8
- * export default artifactConfig({ pages: ["/writeup", "/proposal", "/report"] });
8
+ * export default artifactConfig({ pages: ["/the-read", "/proposal", "/report"] });
9
9
  *
10
10
  * The site's own paths (the `@` alias to its src) are found from the
11
11
  * directory Vite is run in, which is the site's root.
@@ -18,7 +18,7 @@ import { privateChunks } from "./private-chunks.js";
18
18
 
19
19
  export interface ArtifactConfigOptions {
20
20
  /**
21
- * The path of each artifact page the site carries, such as "/writeup".
21
+ * The path of each artifact page the site carries, such as "/the-read".
22
22
  * The root and the 404 page are always prerendered and need no entry.
23
23
  */
24
24
  pages: readonly string[];
@@ -40,7 +40,7 @@ export interface ArtifactPage {
40
40
  export function artifactPages(pages: readonly string[]): ArtifactPage[] {
41
41
  const named = pages.filter((path) => path !== "/" && path !== "/404");
42
42
  return [
43
- // The root holds no document: it sends the reader to /writeup
43
+ // The root holds no document: it sends the reader to /the-read
44
44
  // (vercel.json does the same on the host), and its prerendered
45
45
  // index.html is what a static preview serves at the bare host.
46
46
  { path: "/", prerender: { enabled: true } },
@@ -66,10 +66,10 @@ export function artifactConfig({ pages, extend }: ArtifactConfigOptions): UserCo
66
66
  prerender: {
67
67
  enabled: true,
68
68
  crawlLinks: true,
69
- // The writeup links each screenshot crop to its full image, and
69
+ // The read links each screenshot crop to its full image, and
70
70
  // the crawler follows every href it finds: without this filter it
71
71
  // prerenders /images/<shot>.png as an HTML page and writes that
72
- // over the real file in dist/client (found on a client's writeup,
72
+ // over the real file in dist/client (found on a client's read,
73
73
  // 2026-09-24).
74
74
  filter: ({ path }) => !path.startsWith("/images/"),
75
75
  },