@atelic-action/ui 2.0.0 → 2.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
@@ -20,7 +20,10 @@ 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) |
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
+ | `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) |
24
27
  | `StatRow` | `@atelic-action/ui/components` | A row of large figures with periods, deltas, and trends | [#stat-row](https://ui.atelic.me/#stat-row) |
25
28
  | `Callout` | `@atelic-action/ui/components` | One finding: an icon in its tone, the title, the explainer | [#callout](https://ui.atelic.me/#callout) |
26
29
  | `Disclosure` | `@atelic-action/ui/components` | A section that folds: controlled, an app's card; uncontrolled, a native details fold that prints open | [#disclosure](https://ui.atelic.me/#disclosure) |
@@ -352,7 +355,7 @@ export function ArtifactShell(props: Omit<ArtifactShellProps, "currentPath">) {
352
355
  }
353
356
  ```
354
357
 
355
- 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.
358
+ 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 sign off, the shot, and the shell's logo rule; the grade chip's rules sit in `components.css`, since `ReportCard` wears it) 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.
356
359
 
357
360
  ### The Artifact Vite Config
358
361
 
@@ -694,7 +697,10 @@ The blocks a page composes, each styled in `styles/components.css` inside the `a
694
697
  | Component | What it is | Notes |
695
698
  |---|---|---|
696
699
  | `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 |
697
- | `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 |
700
+ | `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 |
701
+ | `ReportCard` | A name, a grade, and one line on it per row, in labeled groups, each row's parts centered on it | A row with `href` is a link from edge to edge with a chevron at its end; a link inside `line` still goes where it points. `before` sets the earlier grade faded beside the grade; no `grade` draws a dash read as not graded. `legend` explains the letters beside the `title`, or under the card with `legendAt="foot"`; `labels` sets the screen reader's words. The grade is `GradeChip` |
702
+ | `Rank` | A figure with its badge, for a table cell | `figure` is the rank, the place, or a dash for none; `tone` colors the badge and `meaning` names it for a screen reader; `href` links the figure, in a new tab with `newTab`. The figure's slot is a fixed width, so the badges line up down a column |
703
+ | `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 |
698
704
  | `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 |
699
705
  | `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 |
700
706
  | `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 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atelic-action/ui",
3
- "version": "2.0.0",
3
+ "version": "2.2.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",
@@ -18,8 +18,8 @@ export const gradeLabel: Record<WriteupGrade, string> = {
18
18
  /**
19
19
  * The letter as a chip in its grade's color. The documents in
20
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.
21
+ * and so does ReportCard, so its styles live in styles/components.css, which
22
+ * an app may load alone; never in a document's stylesheet.
23
23
  */
24
24
  export function GradeChip({ grade }: { grade: WriteupGrade }) {
25
25
  return (
@@ -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
+ }
@@ -0,0 +1,37 @@
1
+ import { newTabProps } from "../lib/newTabProps";
2
+ import type { Tone } from "./tone";
3
+
4
+ export interface RankProps {
5
+ /** The figure: a rank, a place in an answer, or a dash for none. */
6
+ figure: string;
7
+ /** The tone its badge reads in. */
8
+ tone: Tone;
9
+ /** What the badge means, for a screen reader, e.g. "Top ten". */
10
+ meaning: string;
11
+ /** Where the figure leads, e.g. the page that holds the rank. */
12
+ href?: string;
13
+ /** Open the link in a new tab. */
14
+ newTab?: boolean;
15
+ className?: string;
16
+ }
17
+
18
+ /**
19
+ * A figure with its badge, for a table cell: the badge in its tone on the
20
+ * left, the figure on the right, linked when it has somewhere to go. The
21
+ * figure's slot is a fixed width, so every rank in a column is the same width
22
+ * and the badges line up down it.
23
+ */
24
+ export function Rank({ figure, tone, meaning, href, newTab, className }: RankProps) {
25
+ return (
26
+ <span className={["rank", className].filter(Boolean).join(" ")} data-tone={tone}>
27
+ <span className="chart-swatch" role="img" aria-label={meaning} />
28
+ {href ? (
29
+ <a href={href} {...newTabProps(href, newTab)}>
30
+ {figure}
31
+ </a>
32
+ ) : (
33
+ <span>{figure}</span>
34
+ )}
35
+ </span>
36
+ );
37
+ }
@@ -0,0 +1,153 @@
1
+ import { ChevronRight } from "lucide-react";
2
+ import { Fragment, type ReactNode, useId } from "react";
3
+ import { GradeChip, gradeLabel, type WriteupGrade } from "../artifact/GradeChip";
4
+
5
+ export interface ReportCardRow {
6
+ /** The row's name, e.g. a section's trade name. */
7
+ name: ReactNode;
8
+ /** Where the row leads, e.g. "#technical-seo"; the whole row is the link when set. */
9
+ href?: string;
10
+ /** The grade; a dash, read as not graded, when left out. */
11
+ grade?: WriteupGrade;
12
+ /** The earlier grade, faded beside the grade: what moved. */
13
+ before?: WriteupGrade;
14
+ /** The line behind the grade, in the owner's words. */
15
+ line?: ReactNode;
16
+ }
17
+
18
+ export interface ReportCardGroup {
19
+ /** The label over the group's rows, e.g. "Health". */
20
+ title: string;
21
+ rows: ReportCardRow[];
22
+ }
23
+
24
+ /** The words a screen reader hears around the letters. */
25
+ export interface ReportCardLabels {
26
+ /** Before a grade; "Grade". */
27
+ grade: string;
28
+ /** Before the earlier grade; "Before". */
29
+ before: string;
30
+ /** For a row with no grade; "Not graded yet". */
31
+ notGraded: string;
32
+ }
33
+
34
+ export const reportCardLabels: ReportCardLabels = {
35
+ grade: "Grade",
36
+ before: "Before",
37
+ notGraded: "Not graded yet",
38
+ };
39
+
40
+ export interface ReportCardProps {
41
+ id?: string;
42
+ /** The heading over the card, e.g. "At a Glance". */
43
+ title?: ReactNode;
44
+ /** What a screen reader calls the card when it has no title. */
45
+ label?: string;
46
+ /** What each letter means, in a few words each, right of the title. */
47
+ legend?: Array<{ grade: WriteupGrade; label: string }>;
48
+ /** Where the legend sits: beside the title, or under the card so nothing sits between a heading and the rows. */
49
+ legendAt?: "head" | "foot";
50
+ groups: ReportCardGroup[];
51
+ labels?: Partial<ReportCardLabels>;
52
+ className?: string;
53
+ }
54
+
55
+ /**
56
+ * A report card: a name, a grade, and one line on it per row, in labeled
57
+ * groups, each row's parts centered on it. A group's label
58
+ * is not a heading: the card sits inside a document whose outline the
59
+ * sections already carry. A row with an
60
+ * href is a link from edge to edge, with a chevron at its end; a link inside
61
+ * the line still goes where it points. The grade is the chip every document
62
+ * wears, and the earlier grade, when there is one, sits faded beside it.
63
+ */
64
+ export function ReportCard({
65
+ id,
66
+ title,
67
+ label,
68
+ legend,
69
+ legendAt = "head",
70
+ groups,
71
+ labels: overrides,
72
+ className,
73
+ }: ReportCardProps) {
74
+ // An override left undefined keeps the default, so a caller can pass what it has.
75
+ const titleId = useId();
76
+ const labels = { ...reportCardLabels };
77
+ for (const key of Object.keys(labels) as Array<keyof ReportCardLabels>) {
78
+ const word = overrides?.[key];
79
+ if (word !== undefined) labels[key] = word;
80
+ }
81
+ const shown = groups.filter((group) => group.rows.length > 0);
82
+ const hasLegend = legend !== undefined && legend.length > 0;
83
+ const key = hasLegend && (
84
+ <ul className={legendAt === "foot" ? "rc-legend rc-legend-foot" : "rc-legend"}>
85
+ {legend.map((entry, i) => (
86
+ // biome-ignore lint/suspicious/noArrayIndexKey: a legend is static, and two entries may share a letter
87
+ <li key={`${i}-${entry.grade}`}>
88
+ <GradeChip grade={entry.grade} />
89
+ <span>{entry.label}</span>
90
+ </li>
91
+ ))}
92
+ </ul>
93
+ );
94
+ return (
95
+ <section
96
+ className={["report-card", className].filter(Boolean).join(" ")}
97
+ id={id}
98
+ aria-labelledby={title ? titleId : undefined}
99
+ aria-label={title ? undefined : label}
100
+ >
101
+ {(title || (hasLegend && legendAt === "head")) && (
102
+ <div className="rc-head">
103
+ {title && (
104
+ <h2 className="rc-title" id={titleId}>
105
+ {title}
106
+ </h2>
107
+ )}
108
+ {legendAt === "head" && key}
109
+ </div>
110
+ )}
111
+ <div className="rc-card">
112
+ {shown.map((group, g) => (
113
+ // biome-ignore lint/suspicious/noArrayIndexKey: two groups may share a title
114
+ <Fragment key={`${g}-${group.title}`}>
115
+ <div className="rc-group">{group.title}</div>
116
+ {group.rows.map((row, i) => (
117
+ // biome-ignore lint/suspicious/noArrayIndexKey: rows are static content in a fixed order
118
+ <div className="rc-row" key={i}>
119
+ <span className="rc-grade">
120
+ {row.before && (
121
+ <span className="rc-before">
122
+ <span className="rc-sr">{`${labels.before}: ${gradeLabel[row.before]}. `}</span>
123
+ <span aria-hidden="true">
124
+ <GradeChip grade={row.before} />
125
+ </span>
126
+ </span>
127
+ )}
128
+ {row.grade ? (
129
+ <>
130
+ <span className="rc-sr">{`${labels.grade} ${gradeLabel[row.grade]}: `}</span>
131
+ <span aria-hidden="true">
132
+ <GradeChip grade={row.grade} />
133
+ </span>
134
+ </>
135
+ ) : (
136
+ <>
137
+ <span className="rc-sr">{`${labels.notGraded}: `}</span>
138
+ <span className="rc-dash" aria-hidden="true" />
139
+ </>
140
+ )}
141
+ </span>
142
+ <b className="rc-name">{row.href ? <a href={row.href}>{row.name}</a> : row.name}</b>
143
+ {row.line != null && <span className="rc-line">{row.line}</span>}
144
+ {row.href && <ChevronRight className="rc-go" size={18} aria-hidden="true" />}
145
+ </div>
146
+ ))}
147
+ </Fragment>
148
+ ))}
149
+ </div>
150
+ {legendAt === "foot" && key}
151
+ </section>
152
+ );
153
+ }
@@ -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,7 +23,17 @@ 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";
28
+ export { Rank, type RankProps } from "./Rank";
29
+ export {
30
+ ReportCard,
31
+ type ReportCardGroup,
32
+ type ReportCardLabels,
33
+ type ReportCardProps,
34
+ type ReportCardRow,
35
+ reportCardLabels,
36
+ } from "./ReportCard";
27
37
  export { StackedBar, type StackedBarProps, type StackedBarSegment } from "./StackedBar";
28
38
  export { describeStarRating, StarRating, type StarRatingProps } from "./StarRating";
29
39
  export {
@@ -1,5 +1,6 @@
1
1
  /* The pieces the artifact documents are built from, under src/artifact: the
2
- grade chip, the sign off, the shot, and the shell's logo and print rules. The
2
+ sign off, the shot, and the shell's logo and print rules (the grade chip's
3
+ rules sit in components.css, since ReportCard wears it there). The
3
4
  composed documents (the writeup, the read, the monthly page, the note,
4
5
  and the sealed document stage) and their sheet, with both @page rules,
5
6
  live in @atelic-action/artifact (its styles/documents.css), which a site
@@ -8,58 +9,17 @@
8
9
  a layer still wins over it whatever its specificity. */
9
10
  @layer atelic-ui {
10
11
  /* =================================================================
11
- SHARED: the vocabulary every document wears, the grade chip
12
- (GradeChip), the sign off lockup (SignOff), and the shot (Shot). A
12
+ SHARED: the vocabulary every document wears, the sign off lockup
13
+ (SignOff) and the shot (Shot). A
13
14
  class used by more than one document lives in the base sheet chain
14
15
  rather than in a component's stylesheet, so it can never vanish from
15
16
  a page that does not import the component that defined it.
16
17
 
17
- Tokens only, with one deliberate literal set: the four grade colors.
18
+ Tokens only.
18
19
  ================================================================= */
19
20
 
20
- /* ---- the grade chip ---- */
21
- .atelic-ui .op-grade {
22
- display: inline-block;
23
- vertical-align: 0.1em;
24
- margin-left: 0.6em;
25
- padding: 0.1em 0.55em 0.12em;
26
- border: 1px solid var(--hairline-strong);
27
- border-radius: 999px;
28
- font-family: var(--font-mono);
29
- font-size: 7pt;
30
- font-weight: 600;
31
- letter-spacing: 0.08em;
32
- text-transform: uppercase;
33
- color: var(--ink-soft);
34
- line-height: 1.3;
35
- min-width: 1.6em;
36
- text-align: center;
37
- }
38
- /* The four grade colors are a deliberate literal set: a grade reads
39
- green, yellow, orange, red
40
- everywhere, whatever the brand's palette, so they are not theme tokens.
41
- The orange sits between the yellow's hue and the red's so the four stay
42
- apart on paper, and its letter clears AA on its own tint. */
43
- .atelic-ui .op-grade[data-grade="a"] {
44
- color: #1b7f3b;
45
- border-color: #1b7f3b;
46
- background: #eaf6ee;
47
- }
48
- .atelic-ui .op-grade[data-grade="b"] {
49
- color: #9a6b00;
50
- border-color: #c99a1a;
51
- background: #fdf5dc;
52
- }
53
- .atelic-ui .op-grade[data-grade="c"] {
54
- color: #a84500;
55
- border-color: #e8650a;
56
- background: #fff0e3;
57
- }
58
- .atelic-ui .op-grade[data-grade="d"] {
59
- color: #b3261e;
60
- border-color: #b3261e;
61
- background: #fdecea;
62
- }
21
+ /* The grade chip's rules, its phone size included, live in components.css,
22
+ since ReportCard wears it there and an app may load that sheet alone. */
63
23
 
64
24
  /* ---- the sign off lockup: its container sets the size and the color ---- */
65
25
  .atelic-ui .op-who {
@@ -93,9 +53,6 @@
93
53
  }
94
54
 
95
55
  @media screen and (max-width: 899px) {
96
- .atelic-ui .op-grade {
97
- font-size: 10px;
98
- }
99
56
  .atelic-ui .op-sign {
100
57
  font-size: 22px;
101
58
  }
@@ -390,7 +390,303 @@
390
390
  color: var(--ink-soft);
391
391
  }
392
392
 
393
- /* ---- The charts' shared key: a swatch, a figure, a label ---- */
393
+ /* ---- IconList: a list whose bullets are icons, each in a tone ---- */
394
+ .atelic-ui .icon-list {
395
+ list-style: none;
396
+ margin: 0;
397
+ padding: 0;
398
+ display: grid;
399
+ gap: 8px;
400
+ }
401
+ .atelic-ui .icon-list li {
402
+ display: grid;
403
+ grid-template-columns: auto minmax(0, 1fr);
404
+ column-gap: 10px;
405
+ align-items: start;
406
+ line-height: 1.45;
407
+ text-wrap: pretty;
408
+ }
409
+ /* A list with no tone of its own reads in the text color, whatever tone its ancestor carries; an item's own tone still wins on its row. */
410
+ .atelic-ui .icon-list:not([data-tone]) {
411
+ --tone: currentColor;
412
+ }
413
+ .atelic-ui .icon-list-bullet {
414
+ display: inline-flex;
415
+ margin-top: 0.2em;
416
+ line-height: 0;
417
+ color: var(--tone, currentColor);
418
+ }
419
+
420
+ /* ---- GradeChip: the letter in its grade's color. The four colors are a
421
+ deliberate literal set, green, yellow, orange, red, whatever the
422
+ brand's palette, so they are not theme tokens. It lives here rather
423
+ than in artifact.css because ReportCard wears it, and an app may load
424
+ this sheet alone. ---- */
425
+ .atelic-ui .op-grade {
426
+ display: inline-block;
427
+ vertical-align: 0.1em;
428
+ margin-left: 0.6em;
429
+ padding: 0.1em 0.55em 0.12em;
430
+ border: 1px solid var(--hairline-strong);
431
+ border-radius: 999px;
432
+ font-family: var(--font-mono);
433
+ font-size: 7pt;
434
+ font-weight: 600;
435
+ letter-spacing: 0.08em;
436
+ text-transform: uppercase;
437
+ color: var(--ink-soft);
438
+ line-height: 1.3;
439
+ min-width: 1.6em;
440
+ text-align: center;
441
+ }
442
+ /* The orange sits between the yellow's hue and the red's so the four
443
+ stay apart on paper, and its letter clears AA on its own tint. */
444
+ .atelic-ui .op-grade[data-grade="a"] {
445
+ color: #1b7f3b;
446
+ border-color: #1b7f3b;
447
+ background: #eaf6ee;
448
+ }
449
+ .atelic-ui .op-grade[data-grade="b"] {
450
+ color: #9a6b00;
451
+ border-color: #c99a1a;
452
+ background: #fdf5dc;
453
+ }
454
+ .atelic-ui .op-grade[data-grade="c"] {
455
+ color: #a84500;
456
+ border-color: #e8650a;
457
+ background: #fff0e3;
458
+ }
459
+ .atelic-ui .op-grade[data-grade="d"] {
460
+ color: #b3261e;
461
+ border-color: #b3261e;
462
+ background: #fdecea;
463
+ }
464
+ @media screen and (max-width: 899px) {
465
+ .atelic-ui .op-grade {
466
+ font-size: 10px;
467
+ }
468
+ }
469
+
470
+ /* ---- ReportCard: a name, a grade, and a line per row, in labeled
471
+ groups ---- */
472
+ .atelic-ui .report-card {
473
+ min-width: 0;
474
+ }
475
+ .atelic-ui .rc-head {
476
+ display: flex;
477
+ flex-wrap: wrap;
478
+ align-items: center;
479
+ justify-content: space-between;
480
+ gap: 8px 20px;
481
+ margin-bottom: 10px;
482
+ }
483
+ .atelic-ui .rc-title {
484
+ margin: 0;
485
+ font-family: var(--font-mono);
486
+ font-size: 13px;
487
+ font-weight: 500;
488
+ letter-spacing: 0.1em;
489
+ text-transform: uppercase;
490
+ color: var(--neutral-500);
491
+ }
492
+ .atelic-ui .rc-legend {
493
+ list-style: none;
494
+ margin: 0 0 0 auto;
495
+ padding: 0;
496
+ display: flex;
497
+ flex-wrap: wrap;
498
+ gap: 6px 16px;
499
+ font-size: 13.5px;
500
+ color: var(--ink-soft);
501
+ }
502
+ .atelic-ui .rc-legend-foot {
503
+ justify-content: flex-end;
504
+ margin: 10px 0 0;
505
+ padding: 0 4px;
506
+ }
507
+ .atelic-ui .rc-legend li {
508
+ display: inline-flex;
509
+ align-items: center;
510
+ gap: 7px;
511
+ }
512
+ .atelic-ui .rc-legend .op-grade {
513
+ display: inline-grid;
514
+ place-items: center;
515
+ width: 22px;
516
+ height: 22px;
517
+ margin-left: 0;
518
+ padding: 0;
519
+ border-radius: 6px;
520
+ font-size: 12px;
521
+ line-height: 1;
522
+ }
523
+ .atelic-ui .rc-card {
524
+ display: grid;
525
+ grid-template-columns: auto minmax(0, 1fr) auto;
526
+ background: var(--ui-card, var(--card));
527
+ border: 1px solid var(--neutral-100);
528
+ border-radius: var(--radius-md);
529
+ box-shadow: var(--shadow-sm, none);
530
+ overflow: hidden;
531
+ }
532
+ .atelic-ui .rc-group {
533
+ grid-column: 1 / -1;
534
+ margin: 0;
535
+ padding: 7px 16px;
536
+ background: var(--surface-alt, var(--neutral-100));
537
+ font-family: var(--font-mono);
538
+ font-size: 11.5px;
539
+ font-weight: 500;
540
+ letter-spacing: 0.1em;
541
+ text-transform: uppercase;
542
+ color: var(--ink-soft);
543
+ }
544
+ /* A row is a subgrid of the card, so its parts line up down the card. On
545
+ a phone the name sits over the line; wide, they sit side by side. The
546
+ name's link stretches over the whole row. */
547
+ .atelic-ui .rc-row {
548
+ position: relative;
549
+ grid-column: 1 / -1;
550
+ display: grid;
551
+ grid-template-columns: subgrid;
552
+ grid-template-rows: auto auto;
553
+ column-gap: 12px;
554
+ row-gap: 2px;
555
+ align-items: center;
556
+ align-content: center;
557
+ padding: 12px 12px 12px 16px;
558
+ border-top: 1px solid var(--neutral-100);
559
+ transition: background-color 120ms ease;
560
+ }
561
+ .atelic-ui .rc-grade {
562
+ grid-row: 1 / span 2;
563
+ display: inline-flex;
564
+ align-items: center;
565
+ gap: 6px;
566
+ }
567
+ .atelic-ui .rc-grade .op-grade {
568
+ display: inline-grid;
569
+ place-items: center;
570
+ width: 30px;
571
+ height: 30px;
572
+ margin-left: 0;
573
+ padding: 0;
574
+ border-radius: 9px;
575
+ font-size: 15px;
576
+ line-height: 1;
577
+ }
578
+ .atelic-ui .rc-before .op-grade {
579
+ opacity: 0.45;
580
+ }
581
+ .atelic-ui .rc-dash {
582
+ display: inline-flex;
583
+ align-items: center;
584
+ justify-content: center;
585
+ width: 30px;
586
+ height: 30px;
587
+ color: var(--neutral-500);
588
+ }
589
+ .atelic-ui .rc-dash::before {
590
+ content: "";
591
+ width: 12px;
592
+ border-top: 2px solid currentColor;
593
+ }
594
+ .atelic-ui .rc-name {
595
+ grid-column: 2;
596
+ font-size: 16px;
597
+ font-weight: 650;
598
+ letter-spacing: -0.01em;
599
+ line-height: 1.35;
600
+ color: var(--ink);
601
+ }
602
+ .atelic-ui .rc-name a {
603
+ color: inherit;
604
+ text-decoration: none;
605
+ }
606
+ .atelic-ui .rc-name a::after {
607
+ content: "";
608
+ position: absolute;
609
+ inset: 0;
610
+ }
611
+ .atelic-ui .rc-line {
612
+ grid-column: 2;
613
+ font-size: 15px;
614
+ line-height: 1.45;
615
+ color: var(--ink-soft);
616
+ text-wrap: pretty;
617
+ }
618
+ .atelic-ui .rc-line a {
619
+ position: relative;
620
+ z-index: 1;
621
+ color: var(--ui-primary, var(--primary));
622
+ text-decoration: underline;
623
+ text-underline-offset: 2px;
624
+ }
625
+ .atelic-ui .rc-go {
626
+ grid-column: 3;
627
+ grid-row: 1 / span 2;
628
+ color: var(--neutral-500);
629
+ }
630
+ .atelic-ui .rc-sr {
631
+ position: absolute;
632
+ width: 1px;
633
+ height: 1px;
634
+ margin: -1px;
635
+ padding: 0;
636
+ overflow: hidden;
637
+ clip: rect(0, 0, 0, 0);
638
+ white-space: nowrap;
639
+ border: 0;
640
+ }
641
+ @media print, (min-width: 721px) {
642
+ .atelic-ui .rc-card {
643
+ grid-template-columns: auto 232px minmax(0, 1fr) auto;
644
+ }
645
+ .atelic-ui .rc-group {
646
+ padding: 7px 20px;
647
+ }
648
+ .atelic-ui .rc-row {
649
+ grid-template-rows: auto;
650
+ column-gap: 16px;
651
+ padding: 11px 14px 11px 20px;
652
+ }
653
+ .atelic-ui .rc-grade,
654
+ .atelic-ui .rc-go {
655
+ grid-row: 1;
656
+ }
657
+ .atelic-ui .rc-line {
658
+ grid-column: 3;
659
+ }
660
+ .atelic-ui .rc-go {
661
+ grid-column: 4;
662
+ }
663
+ }
664
+ @media print {
665
+ .atelic-ui .rc-card {
666
+ box-shadow: none;
667
+ border-color: var(--neutral-200);
668
+ }
669
+ .atelic-ui .rc-go {
670
+ display: none;
671
+ }
672
+ }
673
+ @media (prefers-reduced-motion: reduce) {
674
+ .atelic-ui .rc-row {
675
+ transition: none;
676
+ }
677
+ }
678
+ .atelic-ui .rc-row:has(.rc-name a):hover,
679
+ .atelic-ui .rc-row:has(.rc-name a):focus-within {
680
+ background: color-mix(in srgb, var(--ui-primary, var(--primary)) 6%, transparent);
681
+ }
682
+ .atelic-ui .rc-row:has(.rc-name a):hover .rc-name,
683
+ .atelic-ui .rc-row:has(.rc-name a):focus-within .rc-name,
684
+ .atelic-ui .rc-row:has(.rc-name a):hover .rc-go,
685
+ .atelic-ui .rc-row:has(.rc-name a):focus-within .rc-go {
686
+ color: var(--ui-primary, var(--primary));
687
+ }
688
+
689
+ /* ---- The charts' shared key: a swatch, a label, a figure ---- */
394
690
  .atelic-ui .chart-key {
395
691
  list-style: none;
396
692
  margin: 10px 0 0;
@@ -420,6 +716,24 @@
420
716
  flex: none;
421
717
  }
422
718
 
719
+ /* ---- Rank: a figure with its badge; the figure's slot is a fixed
720
+ width, so every rank in a column is the same width and the badges
721
+ line up down it ---- */
722
+ .atelic-ui .rank {
723
+ display: inline-grid;
724
+ grid-template-columns: auto 2.6em;
725
+ column-gap: 8px;
726
+ align-items: center;
727
+ }
728
+ .atelic-ui .rank .chart-swatch {
729
+ justify-self: start;
730
+ }
731
+ .atelic-ui .rank > a,
732
+ .atelic-ui .rank > span:last-child {
733
+ justify-self: end;
734
+ font-variant-numeric: tabular-nums;
735
+ }
736
+
423
737
  /* ---- StackedBar: one bar, the parts of a whole ---- */
424
738
  .atelic-ui .stacked-bar-track {
425
739
  display: flex;
@@ -619,6 +933,9 @@
619
933
  border-bottom: 0;
620
934
  padding-bottom: 0;
621
935
  }
936
+ .atelic-ui .data-table.is-striped tbody tr:nth-child(even) {
937
+ background: color-mix(in srgb, var(--ink) 3%, transparent);
938
+ }
622
939
  .atelic-ui .data-table tbody th {
623
940
  width: 100%;
624
941
  text-align: left;
@@ -686,6 +1003,10 @@
686
1003
  .atelic-ui .data-table [data-align="center"] {
687
1004
  text-align: center;
688
1005
  }
1006
+ .atelic-ui .data-table [data-fit] {
1007
+ width: 1%;
1008
+ white-space: nowrap;
1009
+ }
689
1010
  .atelic-ui .data-table [data-align="right"] {
690
1011
  text-align: right;
691
1012
  }
@@ -719,11 +1040,22 @@
719
1040
  color: var(--ui-primary, var(--primary));
720
1041
  }
721
1042
  .atelic-ui .data-table thead th button svg {
1043
+ flex: none;
722
1044
  opacity: 0.55;
723
1045
  }
724
1046
  .atelic-ui .data-table thead th button[data-active] svg {
725
1047
  opacity: 1;
726
1048
  }
1049
+ /* Striped, the cells take side padding so the band reads, the last column included. */
1050
+ .atelic-ui .data-table.is-striped th,
1051
+ .atelic-ui .data-table.is-striped td {
1052
+ padding-left: 10px;
1053
+ padding-right: 10px;
1054
+ }
1055
+ .atelic-ui .data-table.is-striped th:last-child,
1056
+ .atelic-ui .data-table.is-striped td:last-child {
1057
+ padding-right: 10px;
1058
+ }
727
1059
  .atelic-ui .data-table tbody tr:last-child th,
728
1060
  .atelic-ui .data-table tbody tr:last-child td {
729
1061
  border-bottom: 0;