@atelic-action/ui 2.0.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
@@ -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) |
@@ -694,7 +695,8 @@ The blocks a page composes, each styled in `styles/components.css` inside the `a
694
695
  | Component | What it is | Notes |
695
696
  |---|---|---|
696
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 |
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 |
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 |
698
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 |
699
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 |
700
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 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atelic-action/ui",
3
- "version": "2.0.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",
@@ -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";
@@ -390,7 +390,34 @@
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
+ /* ---- The charts' shared key: a swatch, a label, a figure ---- */
394
421
  .atelic-ui .chart-key {
395
422
  list-style: none;
396
423
  margin: 10px 0 0;
@@ -619,6 +646,9 @@
619
646
  border-bottom: 0;
620
647
  padding-bottom: 0;
621
648
  }
649
+ .atelic-ui .data-table.is-striped tbody tr:nth-child(even) {
650
+ background: color-mix(in srgb, var(--ink) 3%, transparent);
651
+ }
622
652
  .atelic-ui .data-table tbody th {
623
653
  width: 100%;
624
654
  text-align: left;
@@ -686,6 +716,10 @@
686
716
  .atelic-ui .data-table [data-align="center"] {
687
717
  text-align: center;
688
718
  }
719
+ .atelic-ui .data-table [data-fit] {
720
+ width: 1%;
721
+ white-space: nowrap;
722
+ }
689
723
  .atelic-ui .data-table [data-align="right"] {
690
724
  text-align: right;
691
725
  }
@@ -719,11 +753,22 @@
719
753
  color: var(--ui-primary, var(--primary));
720
754
  }
721
755
  .atelic-ui .data-table thead th button svg {
756
+ flex: none;
722
757
  opacity: 0.55;
723
758
  }
724
759
  .atelic-ui .data-table thead th button[data-active] svg {
725
760
  opacity: 1;
726
761
  }
762
+ /* Striped, the cells take side padding so the band reads, the last column included. */
763
+ .atelic-ui .data-table.is-striped th,
764
+ .atelic-ui .data-table.is-striped td {
765
+ padding-left: 10px;
766
+ padding-right: 10px;
767
+ }
768
+ .atelic-ui .data-table.is-striped th:last-child,
769
+ .atelic-ui .data-table.is-striped td:last-child {
770
+ padding-right: 10px;
771
+ }
727
772
  .atelic-ui .data-table tbody tr:last-child th,
728
773
  .atelic-ui .data-table tbody tr:last-child td {
729
774
  border-bottom: 0;