@atelic-action/ui 1.1.0 → 2.1.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
@@ -6,7 +6,7 @@ The package ships source, not a build. Its TSX and CSS arrive as written and com
6
6
 
7
7
  ## Component Index
8
8
 
9
- Every export, grouped by the entry it is imported from. The gallery column links the section of [ui.atelic.me](https://ui.atelic.me) that shows it; none marks an export that draws nothing.
9
+ Every export, grouped by the entry it is imported from. The gallery column links the section of [ui.atelic.me](https://ui.atelic.me) that shows it; none marks an export that draws nothing. The composed documents (the writeup, the read, the monthly page, the note, and the sealed document) are not here: since 2.0.0 they live in [`@atelic-action/artifact`](https://github.com/atelic-action/artifact), which builds them from the pieces this package exports.
10
10
 
11
11
  | Export | Entry | What it is | Gallery |
12
12
  |---|---|---|---|
@@ -20,6 +20,7 @@ Every export, grouped by the entry it is imported from. The gallery column links
20
20
  | `Brand`, `BrandLogo`, `CallToAction`, `HoursRow`, `MenuContact`, `NavLink`, `Phone`, `PostalAddress`, `SiteLink`, `SocialLink`, `PageStop` | `@atelic-action/ui/chrome` | The shapes the chrome renders from (types) | none |
21
21
  | `NotFound` | `@atelic-action/ui/components` | The body of the page an unknown path renders | [#not-found](https://ui.atelic.me/#not-found) |
22
22
  | `Checklist` | `@atelic-action/ui/components` | A short list of checks, each passed or failed | [#checklist](https://ui.atelic.me/#checklist) |
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) |
23
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) |
24
25
  | `StatRow` | `@atelic-action/ui/components` | A row of large figures with periods, deltas, and trends | [#stat-row](https://ui.atelic.me/#stat-row) |
25
26
  | `Callout` | `@atelic-action/ui/components` | One finding: an icon in its tone, the title, the explainer | [#callout](https://ui.atelic.me/#callout) |
@@ -32,12 +33,10 @@ Every export, grouped by the entry it is imported from. The gallery column links
32
33
  | `Watermark` | `@atelic-action/ui/components` | The signed in reader's confidential stamp, pill or tiled | [#watermark](https://ui.atelic.me/#watermark) |
33
34
  | `readCookie`, `Tone` | `@atelic-action/ui/components` | The cookie reader the watermark uses, and the tone type | none |
34
35
  | `Button`, `Chip`, `Eyebrow`, `Lead`, `SectionHeading` | `@atelic-action/ui/primitives` | The small pieces a page is set in | [#primitives](https://ui.atelic.me/#primitives) |
35
- | `Writeup`, `ratingCell` | `@atelic-action/ui/artifact` | The graded document an owner gets, and its star rating table cell | [#writeup](https://ui.atelic.me/#writeup) |
36
- | `Read`, `readLabels` | `@atelic-action/ui/artifact` | The read: a customer's baseline grades explained by cause at the end of the first week, with the plan in order, and its default fixed words | [#read](https://ui.atelic.me/#read) |
37
- | `Report` | `@atelic-action/ui/artifact` | The monthly page an Operate client gets | [#report](https://ui.atelic.me/#report) |
38
- | `Note` | `@atelic-action/ui/artifact` | The first touch page, for a business with no published email | [#note](https://ui.atelic.me/#note) |
39
- | `Gate`, `normalizeEmail` | `@atelic-action/ui/artifact` | The sealed document, opened in the browser by the reader's own address | [#gate](https://ui.atelic.me/#gate) |
40
- | `GradeChip`, `gradeLabel`, `SignOff` | `@atelic-action/ui/artifact` | The grade letter and the sign off the documents share | [#writeup](https://ui.atelic.me/#writeup) |
36
+ | `GradeChip`, `gradeLabel`, `WriteupGrade` | `@atelic-action/ui/artifact` | A section's grade, A to D, as a chip in its color, the letter alone, and the grade type | [#grade-chip](https://ui.atelic.me/#grade-chip) |
37
+ | `SignOff`, `WriteupPerson` | `@atelic-action/ui/artifact` | The close a document ends on, and the person it names | [#sign-off](https://ui.atelic.me/#sign-off) |
38
+ | `Shot`, `WriteupShot` | `@atelic-action/ui/artifact` | The owner's own screen, opening the full picture, over its caption | [#shot](https://ui.atelic.me/#shot) |
39
+ | `Paragraph`, `Runs`, `ArtifactAnchor`, `isEmpty`, `ArtifactLink`, `ArtifactParagraph` | `@atelic-action/ui/artifact` | Plain data text a routine can write as JSON: a paragraph, its runs alone, one link, the empty test, and the two types | [#paragraph](https://ui.atelic.me/#paragraph) |
41
40
  | `ArtifactShell` | `@atelic-action/ui/artifact` | The chrome around a document page: skip link, dark header, credit band, watermark | [#site-header](https://ui.atelic.me/#site-header) (its dark bar) |
42
41
  | `AppErrorBoundary`, `ErrorFallback` | `@atelic-action/ui/artifact` | The error boundary and the page it falls back to | none |
43
42
  | `buildPageHead`, `canonicalUrl`, `newTabProps`, the `ArtifactConfig` types | `@atelic-action/ui/artifact` | A page's head, a link's new tab props, and a site's contract | none |
@@ -126,10 +125,12 @@ Prerendering and server rendering have to compile the package too, so keep it ou
126
125
  ```ts
127
126
  // vite.config.ts
128
127
  export default defineConfig({
129
- ssr: { noExternal: ["@atelic-action/ui"] },
128
+ ssr: { noExternal: ["@atelic-action/ui", "@atelic-action/artifact"] },
130
129
  });
131
130
  ```
132
131
 
132
+ A site that renders a document needs `@atelic-action/artifact` kept internal the same way, since it too ships TSX and CSS as written; a site that renders none lists `@atelic-action/ui` alone.
133
+
133
134
  ## CSS Import Order
134
135
 
135
136
  Import the stylesheets in this order, from the root route or the site's base sheet:
@@ -141,8 +142,9 @@ Import the stylesheets in this order, from the root route or the site's base she
141
142
  5. `@atelic-action/ui/styles/primitives.css`
142
143
  6. `@atelic-action/ui/styles/gate.css`, on a site that mounts the access gate
143
144
  7. `@atelic-action/ui/styles/artifact.css`, on a site that hosts the artifact documents
144
- 8. The site's own CSS
145
- 9. The site's `theme.css`, last
145
+ 8. `@atelic-action/artifact/styles/documents.css`, on the same site, for the documents themselves
146
+ 9. The site's own CSS
147
+ 10. The site's `theme.css`, last
146
148
 
147
149
  Every rule in the package sheets is scoped under `.atelic-ui`, the one root class a consumer wears: a site puts it on its outermost wrapper, an app on the region that holds a package component. Which sheets it imports decides what it gets, so a site importing `base.css` gets the resets and typography under that class and an app importing `components.css` alone gets none of them.
148
150
 
@@ -330,9 +332,9 @@ The two pieces a reader sees are ordinary components, in `@atelic-action/ui/comp
330
332
 
331
333
  ## The Artifact Documents
332
334
 
333
- `@atelic-action/ui/artifact` is everything the artifact template shared across its cuts, so a fix reaches every business's documents with one version bump:
335
+ `@atelic-action/ui/artifact` is the pieces an artifact document is built from and the site around it, so a fix to a piece reaches every business's documents with one version bump. The composed documents themselves (`Writeup`, `Read`, `Report`, `Note`, and the sealed `Gate`, with their content types and their stylesheet) live in [`@atelic-action/artifact`](https://github.com/atelic-action/artifact) since 2.0.0, which depends on this package for everything below:
334
336
 
335
- - **The documents:** `Writeup`, `Read`, `Report`, `Note`, and `Gate` (the sealed document, with its `GatePayload` type; a reader's address is the key, so a payload never lists it, and the component identifies a reader to HubSpot by the address they typed), plus the `GradeChip` and `SignOff` they share, and every content type they take (`WriteupProps`, `ReadProps`, `ReportProps`, `NoteProps`, `GateProps`, and the rest).
337
+ - **The pieces:** `GradeChip` and `gradeLabel` with the `WriteupGrade` type, `SignOff` with the `WriteupPerson` type, `Shot` with the `WriteupShot` type, and the plain data text (`Paragraph`, `Runs`, `ArtifactAnchor`, and `isEmpty`, with the `ArtifactLink` and `ArtifactParagraph` types: a paragraph is a string or runs of text and `{ text, href }` links, so a routine can write a document as JSON).
336
338
  - **The shell:** `ArtifactShell`, the `.atelic-ui` scope with the skip link, the dark header, the credit band, and the watermark around a page.
337
339
  - **The contract and the head:** the `ArtifactConfig` types, and `buildPageHead` and `canonicalUrl` with their `PageMeta` type.
338
340
  - **Who opened it:** `mintToken`, `sealReader`, `openReader`, `readerId`, `readerLink`, and `whoIsReading`. The same six are also built and exported alone as `@atelic-action/ui/artifact/readers`, because a Playwright spec and a script run under Node, which compiles nothing inside `node_modules`.
@@ -351,13 +353,7 @@ export function ArtifactShell(props: Omit<ArtifactShellProps, "currentPath">) {
351
353
  }
352
354
  ```
353
355
 
354
- **The note** is the first touch for a business whose site publishes no email address, served at `/notes` and wrapped in `ArtifactShell` exactly as the writeup is. It is the writeup's sibling, not a variant: a title and a dateline, the opener, two lanes of findings (what the customer sees, what sits under the hood), the steps the work would start with, and the close over the shared `SignOff`. Every prop is plain data, so a routine can write a note as JSON; a paragraph is a string or an array of strings and `{ text, href }` links, and the sign off person's `role` is the one field that also takes a node. A finding is a `Callout` with its `WriteupShot` under it. Its column is the writeup's 920px, centered under the header with the header's `--gutter`, and its text runs the column's full width. A finding sets its screen under its words on a phone and beside them, on the right, when wide and on paper. It reads on a phone first, stacks the two lanes as separate sections top to bottom at every width and on paper, never side by side, and prints to one letter sheet under the writeup's `@page`.
355
-
356
- **The read** is what a customer gets at the end of an engagement's first week, before anything changes: the baseline's grades explained by cause (FOOTPRINT.md, What the Owner Gets). It is the writeup's sibling and wears the writeup's own `.op` root, so its column, head, grade chip and key, tier headings, section cards, screens, sources, close, and print rules are the writeup's; `artifact.css` adds only its `.rd` classes. Top to bottom: a title, the dateline, and a lede of two or three sentences; the gist table (Section, Grade, The Gist), drawn from the tiers so it can never disagree with the body, each section's name linking to its card and a section with no grade showing a quiet dash; then each tier's sections, each headed by its question with its trade name over it and its grade beside it, in three beats (what we found, with an optional short list of examples and any proof pictures in a row under their captions; why, one sentence set apart; what changes) and the tools it was read from; then the plan, numbered, each step a bold title and a sentence or two, with `who` naming a step that is the owner's own; then one closing line over the shared `SignOff`. A later tier (Conversion, once the analytics access lands) is one more entry in `tiers` and appears in the table and the body alike, with no change to the props, and a tier with no sections yet is left out of both. Every prop is plain data, the note's paragraph shape (a string, or runs of text and `{ text, href }` links), so a routine can write a read as JSON. The fixed words (the column heads, the beat labels, the plan's heading) are `readLabels`, and `labels` overrides any of them. A site wraps it in `ArtifactShell` exactly as it wraps the writeup; it sets no `@page`, so the writeup's stays the one that wins.
357
-
358
- **The sealed document's classes.** `Gate` renders the opened document as the HTML it was sealed from, so its markup is the content's, and `artifact.css` styles what that HTML may wear inside `.gate-content`: the tier row (`.gate-tiers` of `.gate-tier` cards, `.is-picked` on the recommended one, `.gate-tier-name`, `.gate-tier-price`, `.gate-tier-note`, and the `.gate-tier-math` sum column of `.gate-tier-was`, `.gate-tier-credit`, `.gate-tier-mathlabel`, and the price), `ul.icon-list` for a list led by inline lucide icons (sized for the card inside a tier), `ul.gate-checks`, and, from 1.0.0 (ATE-599, pulled up from the Wynkoop and Outdoors Geek hosts), the `.gate-addons` row that seats two `.gate-tier.is-addon` monthly cards side by side under the builds, an inline SVG mark inside `.gate-tier-mathlabel` that stays on the label's line, and the `.gate-footnote` line under the tiers that the mark points at. A folder stylesheet that carried any of these deletes it.
359
-
360
- The documents import no stylesheet of their own. A site imports `@atelic-action/ui/styles/artifact.css` in its base sheet after `gate.css` and before its `theme.css`, so every page carries the rules whichever route it is. 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; the order inside the sheet is the order the template's build bundled them in and is load bearing.
356
+ The pieces import no stylesheet of their own. A site imports `@atelic-action/ui/styles/artifact.css` in its base sheet after `gate.css`, then `@atelic-action/artifact/styles/documents.css`, and then its `theme.css`, so every page carries the rules whichever route it is. This sheet holds the pieces alone (the grade chip, the sign off, the shot, and the shell's logo rule) and sets no `@page`; both `@page` rules, the monthly page's and then the writeup's, moved with the documents into their sheet, where their order stays load bearing.
361
357
 
362
358
  ### The Artifact Vite Config
363
359
 
@@ -370,7 +366,7 @@ import { artifactConfig } from "@atelic-action/ui/vite";
370
366
  export default artifactConfig({ pages: ["/writeup", "/proposal", "/report"] });
371
367
  ```
372
368
 
373
- `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"]`. 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.
369
+ `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
370
 
375
371
  A site with a real need of its own passes `extend`, which is merged over the result with Vite's `mergeConfig`:
376
372
 
@@ -402,7 +398,7 @@ The package installs the scripts every artifact site used to carry as files, so
402
398
  | `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` |
403
399
  | `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` |
404
400
  | `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` |
405
- | `atelic-seal-document <config.json> [out.json]` | Seals a document into the payload `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` |
401
+ | `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` |
406
402
  | `atelic-clean-sitemap` | Strips fragment and trailing slash duplicates out of `dist/client/sitemap.xml`; says so and passes when a based build wrote none | A marketing site's `build`, after vite |
407
403
  | `atelic-check-base-path` | Fails the build when a root path in `dist/client` lacks the base path, or `vercel.json` lacks (or strays from) the two rewrites a based cut needs; a `VITE_BASE_PATH` rehearsal only warns about `vercel.json` off Vercel | Last step of a marketing site's `build` |
408
404
  | `atelic-contact-canary` | Posts a token bearing test message to `$SITE_URL/api/contact` and passes only on a Resend id and the expected destination (`CONTACT_EXPECTED_TO`, `CONTACT_CANARY_TOKEN`); a `.invalid` `SITE_URL` is the off switch | `check:contact`, run daily by the canary workflow |
@@ -699,7 +695,8 @@ The blocks a page composes, each styled in `styles/components.css` inside the `a
699
695
  | Component | What it is | Notes |
700
696
  |---|---|---|
701
697
  | `Checklist` | A short list of checks, each passed or failed | Two columns when there is room, one on a phone. `passLabel` and `failLabel` name the icons for a screen reader |
702
- | `DataTable` | A table whose first column names each row, sortable by any column marked `sortable` | A click on a heading sorts on a wide screen. On a phone each row stacks, every fact wearing its column's label, and a dropdown sorts. A cell is a node, or `{ display, sort }` when what it shows is not what it sorts by; a formatted number in a string (`"$12,047"`, `"0.24%"`) sorts as a number. It renders in `defaultSort` on the server |
698
+ | `IconList` | A list whose bullets are icons | `icon` and `tone` on the list set every bullet; an item's own override them. The bullet is decoration, so a screen reader hears only the items. A dot when no icon is set; the text's color when no tone is |
699
+ | `DataTable` | A table whose first column names each row, sortable by any column marked `sortable` | A click on a heading sorts on a wide screen. On a phone each row stacks, every fact wearing its column's label, and a dropdown sorts. A cell is a node, or `{ display, sort }` when what it shows is not what it sorts by; a formatted number in a string (`"$12,047"`, `"0.24%"`) sorts as a number. It renders in `defaultSort` on the server. Every other row sits on a tinted ground; `striped={false}` turns that off. A column marked `fit` hugs its content, for a swatch or a mark beside a figure |
703
700
  | `StatRow` | A row of large figures, each over what it counts and its `period`. `variant="quiet"` is the glance row for a working page: a light figure in the text color over a small capitalized label, hairlines between; `unit` sets a figure's unit small beside it | `trend` draws an arrow before a figure that is itself a change. `deltas` list comparisons beneath: an arrow, how far, against what (`basis`), and the earlier figure (`was`), each in its own tone, since a spend falling and a cost falling read differently |
704
701
  | `Callout` | One finding: an icon in its tone beside the title, the explainer under it | `variant="box"` sets it apart on a tinted ground, for something missing or owed |
705
702
  | `Disclosure` | A section the reader opens and closes, in two forms chosen by whether `open` is passed | Controlled (`open` and `onToggle`), it is an app's card: the caller remembers what was folded, a closed body is not rendered, the label is the toggle, `meta` (a count, a flag) sits beside it outside its name, and `headingLevel` makes the label a real heading. Uncontrolled, it is a document's fold: a native `details` with a `summary` line that carries the point and `defaultOpen`, so it works before any script loads, find in page opens it, and it prints open. `Collapsible`, the fold's old name, was folded in at 0.18.0 |
@@ -892,14 +889,14 @@ An `expiresAt` an hour out keeps the refresh path quiet; one inside five minutes
892
889
 
893
890
  ## The Gallery
894
891
 
895
- `gallery/` is two static pages. The Components shows everything the package exports that draws (the chrome, the page components, the sections, the documents, and the email) on its own with made up data for a fictional bakery, wearing the practice's own palette and type. It lives at [ui.atelic.me](https://ui.atelic.me), noindexed and disallowed in its `robots.txt`, since it is for the people building with the package and never search inventory. It imports the components and stylesheets from `src/` by relative path rather than from npm, so it always shows the code in the repo, and it reads the package name and version from `package.json` at build time. It sits outside `src/`, so it is never published. The second page, [ui.atelic.me/app](https://ui.atelic.me/app) (`gallery/app.html`), runs the app shell for a made up bakery's ops app, on Tailwind through `@tailwindcss/vite`, with its own stylesheet set up the way an app's `globals.css` is; the plugin leaves The Components' plain sheets as they are.
892
+ `gallery/` is two static pages. The Components shows everything the package exports that draws (the chrome, the page components, the sections, the pieces a document is built from, and the email) on its own with made up data for a fictional bakery, wearing the practice's own palette and type. It lives at [ui.atelic.me](https://ui.atelic.me), noindexed and disallowed in its `robots.txt`, since it is for the people building with the package and never search inventory. It imports the components and stylesheets from `src/` by relative path rather than from npm, so it always shows the code in the repo, and it reads the package name and version from `package.json` at build time. It sits outside `src/`, so it is never published. The second page, [ui.atelic.me/app](https://ui.atelic.me/app) (`gallery/app.html`), runs the app shell for a made up bakery's ops app, on Tailwind through `@tailwindcss/vite`, with its own stylesheet set up the way an app's `globals.css` is; the plugin leaves The Components' plain sheets as they are.
896
893
 
897
894
  ```bash
898
895
  bun run gallery:dev # a dev server on the repo's own source
899
896
  bun run gallery:build # the static page, written to gallery/dist
900
897
  ```
901
898
 
902
- `vercel.json` builds it the same way and serves `gallery/dist`. Typecheck and lint cover it, and CI builds it, so a component change that breaks the gallery fails CI. **A new component gets a gallery section in the same change**, with each of its meaningfully different forms shown. `gallery/parts.tsx` holds the frame pieces (`Piece`, `Example`, and `Frame`, which holds a fixed piece inside the page), and a group of pieces lives in a file of its own beside `Gallery.tsx` (`chrome.tsx`, `documents.tsx`, `email.tsx`), mounted there with one element. Each document is one whole fictional document, and the sealed one was sealed with the package's own sealing command (`gallery/gate-payload.json`, reader `reader@example.test`).
899
+ `vercel.json` builds it the same way and serves `gallery/dist`. Typecheck and lint cover it, and CI builds it, so a component change that breaks the gallery fails CI. **A new component gets a gallery section in the same change**, with each of its meaningfully different forms shown. `gallery/parts.tsx` holds the frame pieces (`Piece`, `Example`, and `Frame`, which holds a fixed piece inside the page), and a group of pieces lives in a file of its own beside `Gallery.tsx` (`chrome.tsx`, `artifact.tsx`, `email.tsx`), mounted there with one element. The composed documents are shown by `@atelic-action/artifact`, through its own stubs.
903
900
 
904
901
  ## Releasing
905
902
 
@@ -33,7 +33,8 @@ export function artifactConfig({ pages, extend }) {
33
33
  const config = {
34
34
  // Every stylesheet rides in the one base sheet the root route links
35
35
  // (src/styles/base.css), the documents' own included, since they come from
36
- // @atelic-action/ui. Nothing imports CSS from a component, so there is no
36
+ // @atelic-action/artifact and its pieces from @atelic-action/ui. Nothing
37
+ // imports CSS from a component, so there is no
37
38
  // component bundle to fold: `build.cssCodeSplit: false`, which sat here to
38
39
  // fold it, now fails the build, because TanStack Start looks for the
39
40
  // `style.css` that setting emits and none is written. A site that gives a
@@ -73,10 +74,11 @@ export function artifactConfig({ pages, extend }) {
73
74
  "@": resolve(process.cwd(), "./src"),
74
75
  },
75
76
  },
76
- // @atelic-action/ui ships TSX and CSS as written, so the prerender's server build
77
- // has to compile it rather than hand it to Node as an external.
77
+ // @atelic-action/ui and @atelic-action/artifact (the documents) both ship
78
+ // TSX and CSS as written, so the prerender's server build has to compile
79
+ // them rather than hand them to Node as externals.
78
80
  ssr: {
79
- noExternal: ["@atelic-action/ui"],
81
+ noExternal: ["@atelic-action/ui", "@atelic-action/artifact"],
80
82
  },
81
83
  };
82
84
  return extend ? mergeConfig(config, extend) : config;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atelic-action/ui",
3
- "version": "1.1.0",
3
+ "version": "2.1.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",
@@ -16,9 +16,10 @@ export const gradeLabel: Record<WriteupGrade, string> = {
16
16
  };
17
17
 
18
18
  /**
19
- * The letter as a chip in its grade's color. The writeup and the monthly page
20
- * both wear it, so its styles live in the base sheet chain
21
- * (styles/artifact.css), never in either component's stylesheet.
19
+ * The letter as a chip in its grade's color. The documents in
20
+ * @atelic-action/artifact (the writeup, the read, the monthly page) wear it,
21
+ * so its styles live in the base sheet chain (styles/artifact.css), never in
22
+ * a document's stylesheet.
22
23
  */
23
24
  export function GradeChip({ grade }: { grade: WriteupGrade }) {
24
25
  return (
@@ -1,8 +1,8 @@
1
1
  import { newTabProps } from "../lib/newTabProps";
2
2
 
3
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
4
+ * Plain data text for the documents in @atelic-action/artifact a routine
5
+ * can write as JSON (the note and the read): a paragraph is a string, or runs of text and links in
6
6
  * order. A link opens in a new tab when it leaves the page, and in place
7
7
  * when it is an anchor or a mail link.
8
8
  */
@@ -12,10 +12,12 @@ export interface WriteupShot {
12
12
 
13
13
  /**
14
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.
15
+ * when it has one, over its caption. The writeup, the read, and the note in
16
+ * @atelic-action/artifact all wear it, so its styles (.op-shot) live in the
17
+ * base sheet chain (styles/artifact.css), and each document sizes it in its
18
+ * own sheet. A linked crop says it opens: a hover title, and an expand badge
19
+ * hidden by default that a document may turn on (the note does, on its
20
+ * thumbnails).
19
21
  */
20
22
  export function Shot({ shot }: { shot: WriteupShot }) {
21
23
  return (
@@ -15,8 +15,9 @@ export interface WriteupPerson {
15
15
  /**
16
16
  * The sign off a printed artifact closes on: the script line, then the person
17
17
  * lockup, the photo beside the name, the role, and the address
18
- * (Brand/voice.md, Pitch Surfaces 9). The writeup and the monthly page both
19
- * wear it, so its styles live in the base sheet chain
18
+ * (Brand/voice.md, Pitch Surfaces 9). The documents in @atelic-action/artifact
19
+ * (the writeup, the read, the monthly page, the note) all wear it, so its
20
+ * styles live in the base sheet chain
20
21
  * (styles/artifact.css); its container sets the type size and the color.
21
22
  */
22
23
  export function SignOff({ signOff, person }: { signOff: string; person: WriteupPerson }) {
@@ -1,3 +1,9 @@
1
+ /**
2
+ * The artifact entry: the pieces a document is built from and the site
3
+ * around it. The composed documents (the writeup, the read, the monthly
4
+ * page, the note, and the sealed document) live in @atelic-action/artifact,
5
+ * which builds them from what this entry exports.
6
+ */
1
7
  export { newTabProps } from "../lib/newTabProps";
2
8
  export { AppErrorBoundary } from "./AppErrorBoundary";
3
9
  export { ArtifactShell, type ArtifactShellProps } from "./ArtifactShell";
@@ -9,12 +15,16 @@ export type {
9
15
  GateConfig,
10
16
  } from "./artifactConfig";
11
17
  export { ErrorFallback } from "./ErrorFallback";
12
- export { Gate, type GatePayload, type GatePerson, type GateProps, normalizeEmail } from "./Gate";
13
- export { GradeChip, gradeLabel } from "./GradeChip";
14
- export * from "./Note";
18
+ export { GradeChip, gradeLabel, type WriteupGrade } from "./GradeChip";
19
+ export {
20
+ ArtifactAnchor,
21
+ type ArtifactLink,
22
+ type ArtifactParagraph,
23
+ isEmpty,
24
+ Paragraph,
25
+ Runs,
26
+ } from "./Paragraph";
15
27
  export { buildPageHead, canonicalUrl, type PageMeta } from "./pageHead";
16
- export * from "./Read";
17
- export * from "./Report";
18
28
  export {
19
29
  mintToken,
20
30
  openReader,
@@ -23,5 +33,5 @@ export {
23
33
  sealReader,
24
34
  whoIsReading,
25
35
  } from "./readers";
26
- export { SignOff } from "./SignOff";
27
- export * from "./Writeup";
36
+ export { Shot, type WriteupShot } from "./Shot";
37
+ export { SignOff, type WriteupPerson } from "./SignOff";
@@ -13,6 +13,8 @@ export interface DataTableColumn {
13
13
  sortable?: boolean;
14
14
  /** The direction a first click sorts in; numbers usually read best largest first. */
15
15
  firstSort?: SortDirection;
16
+ /** The column hugs its content (a swatch, a mark) instead of sharing the width. */
17
+ fit?: boolean;
16
18
  }
17
19
 
18
20
  /** A cell whose display is not what it sorts by, e.g. an icon over a number. */
@@ -37,6 +39,8 @@ export interface DataTableProps {
37
39
  defaultSort?: { column: number; direction: SortDirection };
38
40
  /** The label on the phone's sort control. */
39
41
  sortLabel?: string;
42
+ /** Every other row on a tinted ground, so a wide table reads across. On by default. */
43
+ striped?: boolean;
40
44
  className?: string;
41
45
  }
42
46
 
@@ -71,6 +75,7 @@ export function DataTable({
71
75
  rows,
72
76
  defaultSort,
73
77
  sortLabel = "Sort by",
78
+ striped = true,
74
79
  className,
75
80
  }: DataTableProps) {
76
81
  const [sort, setSort] = useState(defaultSort ?? null);
@@ -102,7 +107,7 @@ export function DataTable({
102
107
  };
103
108
 
104
109
  return (
105
- <div className={["data-table", className].filter(Boolean).join(" ")}>
110
+ <div className={["data-table", striped && "is-striped", className].filter(Boolean).join(" ")}>
106
111
  {anySortable && (
107
112
  <div className="data-table-sort">
108
113
  <label htmlFor={selectId}>{sortLabel}</label>
@@ -135,21 +140,20 @@ export function DataTable({
135
140
  {columns.map((column, index) => {
136
141
  const active = sort?.column === index;
137
142
  const Icon = !active ? ArrowUpDown : sort?.direction === "asc" ? ArrowUp : ArrowDown;
143
+ const head = {
144
+ "data-align": column.align ?? "left",
145
+ "data-fit": column.fit || undefined,
146
+ "aria-sort": column.sortable
147
+ ? active
148
+ ? sort?.direction === "asc"
149
+ ? ("ascending" as const)
150
+ : ("descending" as const)
151
+ : ("none" as const)
152
+ : undefined,
153
+ };
138
154
  return (
139
- <th
140
- scope="col"
141
- key={column.label}
142
- data-align={column.align ?? "left"}
143
- aria-sort={
144
- column.sortable
145
- ? active
146
- ? sort?.direction === "asc"
147
- ? "ascending"
148
- : "descending"
149
- : "none"
150
- : undefined
151
- }
152
- >
155
+ // biome-ignore lint/suspicious/noArrayIndexKey: columns sit in order, and two may share a blank label
156
+ <th scope="col" key={index} {...head}>
153
157
  {column.sortable ? (
154
158
  <button
155
159
  type="button"
@@ -171,16 +175,19 @@ export function DataTable({
171
175
  {sorted.map(({ row, index }) => (
172
176
  <tr key={index} data-tone={row.tone}>
173
177
  {row.cells.map((cell, c) => {
174
- const align = columns[c]?.align ?? "left";
175
178
  const body = isValueCell(cell) ? cell.display : cell;
179
+ const shape = {
180
+ "data-align": columns[c]?.align ?? "left",
181
+ "data-fit": columns[c]?.fit || undefined,
182
+ };
176
183
  return c === 0 ? (
177
184
  // biome-ignore lint/suspicious/noArrayIndexKey: cells sit in column order
178
- <th scope="row" key={c} data-align={align}>
185
+ <th scope="row" key={c} {...shape}>
179
186
  {body}
180
187
  </th>
181
188
  ) : (
182
189
  // biome-ignore lint/suspicious/noArrayIndexKey: cells sit in column order
183
- <td key={c} data-align={align} data-label={columns[c]?.label}>
190
+ <td key={c} {...shape} data-label={columns[c]?.label}>
184
191
  {body}
185
192
  </td>
186
193
  );
@@ -0,0 +1,43 @@
1
+ import { Dot } from "lucide-react";
2
+ import type { ReactNode } from "react";
3
+ import type { Tone } from "./tone";
4
+
5
+ export interface IconListItem {
6
+ /** The item itself. */
7
+ body: ReactNode;
8
+ /** The icon it leads with, e.g. a lucide icon at size 16; the list's when left out. */
9
+ icon?: ReactNode;
10
+ /** The tone its bullet reads in; the list's when left out. */
11
+ tone?: Tone;
12
+ }
13
+
14
+ export interface IconListProps {
15
+ items: IconListItem[];
16
+ /** The bullet every item without its own leads with; a dot when left out. */
17
+ icon?: ReactNode;
18
+ /** The tone every bullet without its own reads in; the text's color when left out. */
19
+ tone?: Tone;
20
+ className?: string;
21
+ }
22
+
23
+ /**
24
+ * A list whose bullets are icons: one for the whole list, or one per item,
25
+ * each in a tone set the same two ways. The bullets are decoration; the
26
+ * items carry the meaning, so a screen reader hears the list and not the
27
+ * icons.
28
+ */
29
+ export function IconList({ items, icon, tone, className }: IconListProps) {
30
+ return (
31
+ <ul className={["icon-list", className].filter(Boolean).join(" ")} data-tone={tone}>
32
+ {items.map((item, i) => (
33
+ // biome-ignore lint/suspicious/noArrayIndexKey: items are static content in a fixed order
34
+ <li key={i} data-tone={item.tone}>
35
+ <span className="icon-list-bullet" aria-hidden="true">
36
+ {item.icon ?? icon ?? <Dot size={16} strokeWidth={4} />}
37
+ </span>
38
+ <span className="icon-list-body">{item.body}</span>
39
+ </li>
40
+ ))}
41
+ </ul>
42
+ );
43
+ }
@@ -48,8 +48,8 @@ export function StackedBar({ segments, showKey = true, className }: StackedBarPr
48
48
  {segments.map((segment) => (
49
49
  <li key={segment.label} data-tone={segment.tone} style={fill(segment)}>
50
50
  <span className="chart-swatch" aria-hidden="true" />
51
- <b>{figure(segment)}</b>
52
51
  <span>{segment.label}</span>
52
+ <b>{figure(segment)}</b>
53
53
  </li>
54
54
  ))}
55
55
  </ul>
@@ -23,6 +23,7 @@ export {
23
23
  type DisclosureProps,
24
24
  } from "./Disclosure";
25
25
  export { GateLogin, type GateLoginProps } from "./GateLogin";
26
+ export { IconList, type IconListItem, type IconListProps } from "./IconList";
26
27
  export { NotFound, type NotFoundProps } from "./NotFound";
27
28
  export { StackedBar, type StackedBarProps, type StackedBarSegment } from "./StackedBar";
28
29
  export { describeStarRating, StarRating, type StarRatingProps } from "./StarRating";