@hackcove/ui-react 0.9.1 → 0.10.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.
Files changed (60) hide show
  1. package/MIGRATING.md +21 -0
  2. package/README.md +31 -11
  3. package/dist/Avatar.d.ts +12 -5
  4. package/dist/Avatar.js +17 -12
  5. package/dist/AvatarStack.js +2 -1
  6. package/dist/Button.d.ts +15 -1
  7. package/dist/Button.js +17 -6
  8. package/dist/Card.d.ts +8 -1
  9. package/dist/Card.js +15 -6
  10. package/dist/CardParts.d.ts +2 -1
  11. package/dist/CardParts.js +6 -2
  12. package/dist/Checkbox.d.ts +1 -1
  13. package/dist/Checkbox.js +1 -13
  14. package/dist/ColourSchemeToggle.d.ts +4 -2
  15. package/dist/ColourSchemeToggle.js +23 -10
  16. package/dist/Dialog.d.ts +2 -1
  17. package/dist/Dialog.js +44 -3
  18. package/dist/Disclosure.d.ts +10 -1
  19. package/dist/Disclosure.js +13 -3
  20. package/dist/Field.d.ts +9 -1
  21. package/dist/Field.js +5 -3
  22. package/dist/IconButton.d.ts +2 -0
  23. package/dist/IconButton.js +2 -2
  24. package/dist/Image.d.ts +14 -4
  25. package/dist/Image.js +22 -6
  26. package/dist/InlineEdit.d.ts +22 -0
  27. package/dist/InlineEdit.js +60 -0
  28. package/dist/KeyValueList.d.ts +9 -2
  29. package/dist/KeyValueList.js +8 -3
  30. package/dist/Lightbox.d.ts +2 -0
  31. package/dist/Lightbox.js +2 -1
  32. package/dist/ListRow.d.ts +11 -0
  33. package/dist/ListRow.js +28 -6
  34. package/dist/MediaTile.d.ts +11 -3
  35. package/dist/MediaTile.js +10 -7
  36. package/dist/PageLayouts.d.ts +22 -21
  37. package/dist/PageLayouts.js +17 -21
  38. package/dist/SideColumn.d.ts +26 -0
  39. package/dist/SideColumn.js +22 -0
  40. package/dist/SidebarLayout.d.ts +24 -0
  41. package/dist/SidebarLayout.js +22 -0
  42. package/dist/Stack.d.ts +10 -2
  43. package/dist/Stack.js +5 -3
  44. package/dist/StatusPages.d.ts +13 -3
  45. package/dist/StatusPages.js +8 -5
  46. package/dist/Switch.d.ts +42 -0
  47. package/dist/Switch.js +56 -0
  48. package/dist/TableEditCell.d.ts +9 -7
  49. package/dist/TableEditCell.js +12 -53
  50. package/dist/foldedColumn.d.ts +18 -0
  51. package/dist/foldedColumn.js +45 -0
  52. package/dist/index.d.ts +5 -1
  53. package/dist/index.js +5 -1
  54. package/dist/labelWords.d.ts +12 -0
  55. package/dist/labelWords.js +14 -0
  56. package/dist/scrollStop.d.ts +27 -0
  57. package/dist/scrollStop.js +68 -0
  58. package/dist/stickyColumn.d.ts +17 -0
  59. package/dist/stickyColumn.js +17 -0
  60. package/package.json +2 -2
package/MIGRATING.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Migrating @hackcove/ui-react
2
2
 
3
+ ## 0.9 → 0.10: Avatar `2xl` is 64px, the old 96px is `3xl`
4
+
5
+ `Avatar` gained a 64px size. The sizes keep their order (`xs`, `sm`, `md`, `lg`, `xl`, `2xl`, `3xl`),
6
+ so the new one is `2xl` and the 96px one moved up to `3xl`. There is no alias, and **TypeScript does
7
+ not flag it**: `size="2xl"` still compiles and now draws 64px, not 96px.
8
+
9
+ 1. Find every `size="2xl"` on an `Avatar` or an `AvatarStack` and write `size="3xl"`.
10
+ 2. Where a 64px avatar was wanted all along (a local one drawn with `size-16`), use `size="2xl"`.
11
+
12
+ New, nothing to change: `Avatar` takes `onClick` (the whole avatar becomes one button, named by `name`);
13
+ `MediaTile` takes `aspect="natural"` and `onRemove`; `Image` takes `aspect="natural"` and `framed`
14
+ (a white mat that stays white in every theme). A theme of your own needs one rule for the mat, as
15
+ the `@hackcove/themes` files have:
16
+ `.your-scope .bg-white\/100 { background-color: #ffffff; }`.
17
+
18
+ `AppShell` renders its own `<main>`, so a page inside it must not render another; a page's root is a `<div>` or a fragment.
19
+
20
+ `ColourSchemeToggle` is named by its content now, so it is right before hydration with no script: a
21
+ test that reads its `aria-label` attribute reads the name instead (`getByRole("button", { name })`,
22
+ `toHaveAccessibleName`). The name is the same; `title` appears only after hydration.
23
+
3
24
  ## 0.7 → 0.8: `@hackcove/ui` is now `@hackcove/ui-react`
4
25
 
5
26
  Nothing else changed: 0.8.0 is the 0.7.1 components under a new name, so the React package pairs
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @hackcove/ui-react
2
2
 
3
3
  Hack Cove React components, written against Tailwind v4 utilities. They carry no theme: pair them
4
- with a `@hackcove/theme-*` package such as `@hackcove/theme-catppuccin-mocha` and put its scope
4
+ with a theme from `@hackcove/themes`, such as `@hackcove/themes/catppuccin-mocha.css`, and put its scope
5
5
  class on `<html>` (`.dark` for Mocha). With no theme they render in Tailwind's own default colours,
6
6
  a plain light look; it works, but it is not a look Hack Cove designs or checks.
7
7
 
@@ -56,11 +56,11 @@ root (`DateField`) puts `data-*` on its wrapper and `aria-describedby` on the fi
56
56
 
57
57
  One scale runs through the library: `Size` is `"sm" | "md" | "lg"`, and `md` is the default
58
58
  wherever a component takes `size`. The controls (`Button` (also as a link), `IconButton`,
59
- `ColourSchemeToggle`, `Input` (also `type="search"`), `Select`, `Textarea`, `Checkbox`, `RadioGroup`,
59
+ `ColourSchemeToggle`, `Input` (also `type="search"`), `Select`, `Textarea`, `Checkbox`, `Switch`, `RadioGroup`,
60
60
  `DateField` (also with `calendar`), `SegmentedControl`, `Tabs variant="pill"`)
61
61
  share heights at each step (`sm` 26px, `md` 38px, `lg` 46px, the same on a `Select` and a `DateField` as on an `Input`; a Playwright check measures them side by side in every theme), so a row of mixed controls at one size lines up. `Card`, `ListRow`,
62
62
  `FileDrop`, `Dialog`, `Drawer`, `Avatar`, `SubsectionHeading`, `TextLink` and the bars take the
63
- same three names. Only `Avatar` (`xs`, `xl`, `2xl`) and the `Stack` gap (`xs`, `xl`) go beyond them.
63
+ same three names. Only `Avatar` (`xs`, `xl`, `2xl`, `3xl`) and the `Stack` gap (`xs`, `xl`) go beyond them.
64
64
 
65
65
  One default is not `md`, on purpose: `Cluster`'s gap is `xs` (a row is a label and its chips,
66
66
  which sit tight). `TextLink` with no `size`
@@ -68,10 +68,10 @@ takes the size of the text around it. Upgrading: `MIGRATING.md`, shipped in the
68
68
 
69
69
  ## What is exported
70
70
 
71
- - Primitives: `Card` (a bordered panel; `title` and `count` make a small heading, and `status` (a `Pill` or any node) sits right after them, where `actions` sits at the far end; `flush` drops the padding and clips the corners, for a table that runs edge to edge; `bar` moves the title, count and `actions` into a tinted strip over the body; `dashed` is an empty-state shell with centred muted text, and `dashed="filled"` the same on a surface fill with more padding; `badge` pins a `Pill` to the top-right corner over the content, on a panel or a link card, where it is part of the link's name; `icon` puts one of the `Icon` set's names above the title at `feature` size in the accent colour, decorative, on a panel or a link card but not a `bar`, `flush` or `dashed` one; `attention` draws the strong warn edge (`border-amber-600`) round a card that needs a look, with or without `tone="warn"`, and does nothing on a `dashed` card; `footer` puts a tinted strip under a hairline across the bottom, for the card's own actions or a total: it takes the padding off the root and gives it to the body, so the strip runs edge to edge and stays at the foot of a card stretched in a `CardGrid`; not on a `dashed` card or a card that is a link; `inset` is a full-bleed band (a progress bar, a chart, an image) under the title and over the children, running edge to edge of a padded card and not on a `dashed` card; a bordered sub-card inside a card is a nested `Card tone="neutral"`; `as` is `section` (default), `article` or `div`; `id`, `data-*` and `aria-*` go on the root (on a link card too: the link inside takes only its own `aria-label`, `aria-labelledby` and `aria-current`); a titled panel is named by its title through `aria-labelledby`, unless you pass your own `aria-label` or `aria-labelledby`; a Card with an `id` is an anchor target, scrolled clear of the sticky header and outlined in violet while it is the `:target`), `EpicHeader`, `Pill` (a small tinted label; `tone`, `dot`, `round` and `title`; `label` makes it a two-tone `label: value` pill, the label keeping the tint and the children, the value, on the surface; `href`/`render` make it a link and `onClick` a button; `onRemove` with `removeLabel` adds an x button, a separate button beside a link or button pill and never inside it), `groupByEpic`, `Markdown`, `Badge` (a small red count for sitting on an icon; `count` above 99 reads `99+`, `corner` pins it to the top-right of its `relative` parent; an `IconButton` takes `badge` and does this itself).
71
+ - Primitives: `Card` (a bordered panel; `title` and `count` make a small heading, `titleStyle="name"` sets the title in sentence case in the primary ink for a card that is a named thing (the default `overline` is small uppercase), and `status` (a `Pill` or any node) sits right after them, where `actions` sits at the far end; `flush` drops the padding and clips the corners, for a table that runs edge to edge; `bar` moves the title, count and `actions` into a tinted strip over the body; `dashed` is an empty-state shell with centred muted text, and `dashed="filled"` the same on a surface fill with more padding; `badge` pins a `Pill` to the top-right corner over the content, on a panel or a link card, where it is part of the link's name; `icon` puts one of the `Icon` set's names above the title at `feature` size in the accent colour, decorative, on a panel or a link card but not a `bar`, `flush` or `dashed` one; `attention` draws the strong warn edge (`border-amber-600`) round a card that needs a look, with or without `tone="warn"`, and does nothing on a `dashed` card; `footer` puts a tinted strip under a hairline across the bottom, for the card's own actions or a total: it takes the padding off the root and gives it to the body, so the strip runs edge to edge and stays at the foot of a card stretched in a `CardGrid`; not on a `dashed` card or a card that is a link; `inset` is a full-bleed band (a progress bar, a chart, an image) under the title and over the children, running edge to edge of a padded card and not on a `dashed` card; a bordered sub-card inside a card is a nested `Card tone="neutral"`; `as` is `section` (default), `article` or `div`; `id`, `data-*` and `aria-*` go on the root (on a link card too: the link inside takes only its own `aria-label`, `aria-labelledby` and `aria-current`); a titled panel is named by its title through `aria-labelledby`, unless you pass your own `aria-label` or `aria-labelledby`; a Card with an `id` is an anchor target, scrolled clear of the sticky header and outlined in violet while it is the `:target`), `EpicHeader`, `Pill` (a small tinted label; `tone`, `dot`, `round` and `title`; `label` makes it a two-tone `label: value` pill, the label keeping the tint and the children, the value, on the surface; `href`/`render` make it a link and `onClick` a button; `onRemove` with `removeLabel` adds an x button, a separate button beside a link or button pill and never inside it), `groupByEpic`, `Markdown`, `Badge` (a small red count for sitting on an icon; `count` above 99 reads `99+`, `corner` pins it to the top-right of its `relative` parent; an `IconButton` takes `badge` and does this itself).
72
72
  - Data: `StatTile` (one headline figure: an uppercase `label` over a large tabular `value`, already formatted, an optional `hint` under it; `tone` is `neutral` (default), `positive` or `negative`, and `accent` adds a bar across the top in the same tone), `ProgressBar` (a thin bar with a filled share of `max` (default 100); `label` names it for a screen reader, which also hears the percentage; `tone` is `accent` (default), `positive`, `warn` or `negative`), `SplitBar` (a bar split between `a` and `b` by their shares; `names` says whose is whose to a screen reader; `toneA` (default `negative`) and `toneB` (default `positive`)), `StackedBar` (any number of `segments`, each `{ label, value, tone }`, sized by its share of the sum; `legend` draws a `Legend` under it) and `Legend` (a swatch, a `label` and an optional `detail` per item, the key to a chart's or a row stripe's colours). The three bars take `size`: `sm` 6px, `md` 8px (default), `lg` 12px.
73
73
  - Status pages: `NotFound` (404), `ErrorPage` (500 or any failure; `detail` is error text shown in a `CodeBlock`, never a stack trace, and `onRetry` adds "Try again"), `Offline` (`onRetry` too), `Maintenance` and `Forbidden` (403). Each is a whole page with default copy: `title` and `children` replace the heading and the body, `action` is the primary action, and `homeHref`, or `renderHome` for the app's own router `<Link>`, adds a "Back to home" button. With neither there is no home link.
74
- - Layout (see Layout): `AppShell`, `AppShellBrand`, `appShellRootClassFor`, `Container`, `PageHeader`, `Stack`, `Section`, `Cluster`, `ClusterGrow`, and the page layouts `ListLayout`, `DetailLayout`, `FormLayout`, `ProseLayout`, `CardGrid`, `SidebarLayout`, `FullBleedLayout`, `CenteredLayout`.
74
+ - Layout (see Layout): `AppShell`, `AppShellBrand`, `appShellRootClassFor`, `Container`, `PageHeader`, `Stack`, `Section`, `Cluster`, `ClusterGrow`, `SideColumn`, and the page layouts `ListLayout`, `DetailLayout`, `FormLayout`, `ProseLayout`, `CardGrid`, `SidebarLayout`, `FullBleedLayout`, `CenteredLayout`.
75
75
  - Typography roles (each pins size + weight, takes a `color` and an `as`): `Display`, `Lead`,
76
76
  `PageHeading`, `SectionHeading`, `SubsectionHeading`, `Overline`, `Text`, `Caption`. Opt-in props, none changing today's look: `SectionHeading rule`
77
77
  (a hairline under it, with its spacing), `SubsectionHeading size` (`sm`, `md` default, `lg`) and `truncate` (one line with an ellipsis),
@@ -80,7 +80,7 @@ takes the size of the text around it. Upgrading: `MIGRATING.md`, shipped in the
80
80
  text colours as a `TableCell` tone, winning over `color`; pair the colour with a word, as in "Saved."),
81
81
  `numeric` (tabular figures), `mono` (the monospace face) and `truncate` (one line, cut with an
82
82
  ellipsis, the full text in a `title`, so its content is text).
83
- - Controls: `Button` (`variant` is `primary` (default), `outline`, `ghost`, `link` or `dashed`, the link having no box: text with an underline on hover, for an in-line action, and the dashed one a dashed outline in muted text that fills its container, like `Card dashed`, for an "add another" row, with a leading icon as a child, ignoring `tone`; `size="sm"` is compact, `text-xs`, for table rows and toolbars, `size="lg"` is `text-base` with more padding; `tone="danger"` is red, for delete and clear, and `tone="positive"` green, for accept and approve; `disabledReason` disables it, greys it flat and makes the reason its `title`; `busy` disables it with `aria-busy` and keeps its colour, showing `busyLabel` in place of the children; `pressed` makes it a toggle with `aria-pressed`, on `outline` and `ghost` neutral when off and filled in the tone's primary colour when on, so an `outline` `size="sm"` Button with `pressed` is a filter chip; with `href`, or `render` to draw the app's own router `<Link>`, it is a link with the same look, `external` opening a new tab), `ButtonGroup` (a wrapping row of buttons; `end` is one item pushed to the far side, `label` makes it a named `role="group"`), `IconButton` (a square button holding one icon; `label` is required and becomes its `aria-label` and `title`; `variant` is `outline` (default) or `ghost`, `tone` is `default` or `danger`, `size` matches a `Button`'s height; `badge` puts a red count on its corner, not shown at 0; with `href` or `render` it is a link, `external` opening a new tab), `Icon` (one bare `<svg>` sized to fill a box, or `name` for one of the set's own icons (`diff`, `baseline`, `review`, `ci`, `self-host`, `privacy`, `capture`, `branch`, `theme`, `speed`: one 24 grid, a 1.5 stroke, round caps and joins, each the idea and not a product): `size` `sm` 14px, `md` 16px (default), `lg` 20px or `feature` 32px; `tone` is a text tone or `inherit` (default, the text colour around it); `aria-hidden` unless `label` names it as `role="img"`), `ColourSchemeToggle` (a light/dark switch on `<html>`; `id`, `data-*` and other button props go on the button, `store={false}` skips `localStorage`; see Light and dark), `Input` (a text input; `icon` adds a leading icon inside the field; `prefix` is short text inside the border before the value, a currency symbol, on a plain field only; `type="search"` drops the browser's search look and shows a "Clear search" button while the field has a value), `Textarea` (a multi-line text input, `rows` defaults to 3), `Select` (a native `<select>` in the control look; takes `<option>` children and keeps the browser's arrow). `Input`, `Select` and `Textarea` take `size` (`md` default, `sm`: `text-xs`, less padding, for dense forms and table rows, or `lg`: `text-base`, more padding) and `width` (`full` default, the fixed `xs` 5rem, `sm` 7rem, `md` 9rem, `lg` 12rem, `fill`: grows to the room left in a flex row and can shrink to nothing, so the buttons beside it stay on the line, or `auto`: no width class, as wide as the browser makes it); a fixed width replaces `w-full`, so there is no class to collide with. `Input` also takes `align` (`start` default, or `end` to right-align a numeric value). `size` replaces the native `size` attribute, so a multi-row `<select size={4}>` is not reachable through `Select`. `Checkbox` (a native checkbox with its label; other props go on the input; `size="sm"` is `text-xs` with a smaller box and `size="lg"` `text-base` with a larger one, `align="start"` puts the box on the first line of a label that wraps, where the default `center` centres it on all of them, and with no `label` it is a bare box for a table cell, named by `aria-label`; `description` is a muted line under the label, the box's `aria-describedby`). `RadioGroup` and `Radio` (one choice from a few options: the group has a `label`, `value`/`onChange` or `defaultValue`, `name`, `isDisabled`, `isRequired`, `size` (`sm`, `md` default, `lg`, as `Checkbox`) and an `error` shown like `Field`'s with `role="alert"`; each `Radio` has a `value`, a `label`, an optional `description` and `isDisabled`; one tab stop, the arrow keys move and choose). `Disclosure` (a summary button with a chevron and `aria-expanded` over a body that opens and closes; `summary`, `children`, `defaultOpen`; it keeps its own state and replaces `<details>`). `FileDrop` (a dashed drop zone, a `<label>` around a file input that covers it, so a click opens the dialog and a dropped file lands on the input; `children` is the prompt, `accept`, `multiple`, `name`, `disabled` and `onChange` on the input). `CopyButton` (a small text button that writes `text` to the clipboard and swaps `label` ("copy") for `copiedLabel` ("copied"); every other button prop, `disabled` included, goes on the `<button>`). `CopyLine` (one line of mono text in a box with a `CopyButton` beside it; `disabled` disables the button; other props go on the root `<div>`). `Field` (a label above a control, `htmlFor` names the control's id; a `hint` sits under it, and an `error` replaces the hint with a `role="alert"` line; `span="full"` runs it across both columns of a `FormGrid`). `FormGrid` (two columns of `Field`s from `md`, one below it), `FormRow` (an inline-edit row: `FormRowField`s side by side, an `action` after them, and one `hint` or `error` under the whole row; see Layout) and `FormRowField` (a compact label over one control, `span` 1 to 4). `Callout` (a tinted notice box; `tone` is `neutral`, `info` (default), `positive`, `warn` or `danger`; an optional `title` sits in bold above the body; `actions` puts a button or link at the end of the row; `banner` makes it a flat strip, bottom border only and no radius, for the top of a `Card flush`). `ListRow` (a full-width row that is a `<button>`, or a link with `href` or `render`, with a hover fill, a left-aligned label and `trailing` content; `divided` draws a hairline above the row, to separate rows in a `Card flush`; `expanded` makes it the header of a collapsible section: `aria-expanded` and a chevron, the caller owning the state and the section; `leading` is content before the label (an `Avatar`), and `leadingWidth="thumbnail"` gives it a fixed width so rows with and without a picture line up; `size` sets the padding (`sm` dense, `md` default, `lg`); `focused` draws the focus ring on the row the keyboard cursor is on, for a list moved with the arrow keys while focus stays on the list; `selected` marks the row in view with a violet fill and `aria-current="true"`; a link row (`href` or `render`) can hold its own link, button or control in `trailing`, which sits above the row's link and takes its own click; `static` makes it a plain `<div>`, for a row that holds its own buttons where the row would otherwise be a `<button>`, and adds `actions`, buttons after `trailing`, and `revealOnHover`, which shows them only on hover or focus inside the row and always on a touch screen; a compact bordered list is `Card flush` around `static` rows at `size="sm"`, `divided` after the first (no separate part); `active` marks the row the pointer or the keyboard is on, with the hover fill and the `revealOnHover` actions shown, for a list that follows both through the row's `onPointerEnter`, `onPointerLeave` and `onFocus`, which every row passes through). `Tabs` (underline tabs for switching a view in place: a row over a bottom rule, the current tab underlined in the accent; `items` are `{ value, label }` buttons, with `value` the current one's `value` and `onChange(value)` the caller's state, one tab stop and the arrow keys, Home and End moving between enabled tabs, or `{ label, href | render, active?, external? }` links in a `<nav>` with `aria-current="page"` on the current one (`active`, or the item whose `value` is `value`); `label` names the group; a button tab takes `disabled` and `controls`, the id of its `role="tabpanel"`, which is the caller's; `variant="pill"` fills the current tab, with no rule, for `PageHeader`'s `tabs` slot, an `AppShell` `nav` or a rail; only a pill row takes `size` and `orientation`, and `orientation="vertical"` stacks it in one column, the content of a `SidebarLayout` rail; `disabled` on the row disables every tab, e.g. until hydration). `SegmentedControl` (a joined pill group, one option filled; the same `items`, `value`, `onChange`, `disabled` and `label` as `Tabs`, as a `radiogroup` with `aria-checked` on each `radio`). On both an item takes `data-*` attributes (typed `data-${string}`) onto its button or link; on `Tabs` and `SegmentedControl` an item takes `count` (a small muted number after the label, part of its name: "Shots 12") and `title`; a `disabled` button option stays in the page as `aria-disabled`, so its `title` is still the hover tooltip and is announced as the reason, it is out of the tab order and the arrow keys, Home and End skip it. `Dialog` (a modal on the browser's `<dialog>`, opened on mount, so render it only while open: `title` is its name, `description` a line under it, `footer` the right-aligned actions; Esc, the backdrop or a `<form method="dialog">` call `onClose`; `size` is `sm` (320px, a list), `md` (440px, default, a form), `lg` (640px, a wider form or a table) or `full` (the whole screen, for media or a player: the body fills the height between the title and the footer and scrolls if taller; there is no backdrop to click, so put a close button on a `<form method="dialog">` in the `footer`)). `FieldDialog` (a `Dialog` with one text field and Cancel and submit buttons, every string passed in: `title`, `description`, `label`, `inputLabel`, `placeholder`, `hint`, `emptyReason`, `submit` and `busyLabel`; `onSubmit` gets the trimmed value and a resolved promise closes it, a rejection shows its message). `Drawer` (a modal slide-over on the right edge, on a native `<dialog>` opened on mount like `Dialog`, so render it only while open: `title` is its name, a close button sits beside it, `children` scroll between the header and an optional pinned `footer`; Esc, the scrim and the close button call `onClose`; `size` is `sm`, `md` (default) or `lg`; `closeLabel` names the close button). `Breadcrumbs` (a `<nav aria-label="Breadcrumb">` over an ordered list; `crumbs` are nodes, or `{ label, href | render, external? }` for one that links; the last is the current page and is never a link and gets `aria-current="page"`, and each crumb truncates). `Table` (a list of records in a rounded, bordered card around a full-width `<table>` that scrolls sideways on a narrow screen; `caption` names it for assistive tech and is not drawn; compose it from `TableHead`, `TableBody`, `TableFoot`, `TableRow`, `TableHeaderCell` (`align` `start`, `center` or `end`; `srLabel` is a visually hidden column name, in place of children, for a column of row actions: "Actions") and `TableCell` (`align`, `indent` of 1 to 3 to set a start-aligned cell in, for a line under a parent row, `tone` `default`, `strong` or `muted`, or the Callout tones `neutral`, `info`, `positive`, `warn` or `danger` for an amount that means something, in the tone's text colour and with no fill, so pair the colour with a sign or a word; `numeric` for tabular figures, `mono` for a small mono face, `truncate` to cap a long value's width, cut it with an ellipsis and put the full text in a `title`, so its content is text (strings and numbers); `secondary` for a second, muted line under the content; `colSpan` passes through, so a `TableCell` with `colSpan` in a row of its own is a full-width panel, an empty state or a note, centred with `align="center"`); `TableEditCell` (a `TableCell` whose value opens an editor in place: `children` is the value, drawn as a button; a click, Enter or Space swaps it for `renderEditor(close)`, a plain control in the cell and not a popover of its own, and moves the focus into it; `close` and Esc put the focus back on the value, and a press outside or Tab out of it closes it too, so the control saves on its own change or submit; `label` names the button and the editor; it takes the `TableCell` props except `truncate`); `TableFoot` (the `<tfoot>` for totals: its cells are strong by default and each row has a firmer line above it, in a card and in a `plain` table alike); `TableGroupRow` (a full-width group header in the body: `label` spans `colSpan` columns, the table's column count, which is required; optional `trailing` content sits at the far end, and the group's rows follow it); `tone` on a `TableRow` in the body marks the row with a stripe on its leading edge over a soft fill: `neutral`, `info`, `positive`, `warn` or `danger`, as `Callout`, or `muted`, which only fades the row (a transfer, a row that is not the point); a bold subtotal is not a tone: `emphasis` on a `TableRow` in the body gives it a `TableFoot` row's look among the other rows; `stripe` (a text tone) draws the leading stripe alone, with no fill; a row takes one of `tone`, `stripe` and `emphasis`; `variant="plain"` on `Table` drops the card border, fill and shadow, the head strip and the row hover and keeps the row dividers, for a table inside a card of its own; every table element passes `id`, `data-*` and `aria-*` through, and none takes a `className` or `style`). `Timeline` (a named `<ol>` of events; `label` is its `aria-label`, `empty` is drawn instead when there are no items) with `TimelineItem` (a `title`, an already-formatted `time`, the `children` as a note, and `tone`, a Callout tone for the marker dot, `neutral` by default). `KeyValueList` (a `<dl>` of label/value rows; each `KeyValue` takes a `label` and its value as children, the value right-aligned in tabular figures; `align="start"` puts the values left-aligned in one column beside the labels instead, using CSS subgrid: in Chrome before 117 a row stacks its label over its value). `Avatar` (a round badge of the initials of `name`, titled with the name and `aria-hidden`; with `src` (the app's own local image path, never a remote one) it is a round, cropped `<img alt={name}>` instead, and if that image fails to load it shows the initials; `size` is `xs` (16px, for an inline mention), `sm`, `md` (default), `lg`, `xl` (56px) or `2xl` (96px); `ring` adds a page-colour border and a negative margin for an overlapping stack; the colour is one of seven stock background classes picked by a hash of the name, remapped by each theme, and `avatarColourClass(name)` returns it to colour something else the same way; `initialsOf(name)` is the two letters). `TextLink` (an inline text link in the accent colour; `href` is required unless `render` draws the app's own router `<Link>`, `external` adds `target="_blank"` and `rel="noreferrer"`, `stretch` covers the nearest `relative` ancestor so a whole row or card is the click target, `size` (`sm`, `md` or `lg`) pins the type size where it would otherwise follow the text around it, `mono` sets the monospace face, `tone` (`warn`, `danger`, `positive`, `info` or `muted`) gives it a meaning's colour in place of the accent, every other anchor prop passes through). `MetaList` (a wrapping row of facts or links in muted small text, joined by an `aria-hidden` `separator`: `·` (default), `•` or `none`; branch · SHA · date, or a footer's Privacy · Terms; an item is text, a `Code` or a `TextLink`; `nowrap` keeps each item on one line, for links; `mono` sets the row in the monospace face, for ids and paths). `PageFade` (the page's content area, named for the View Transitions API so a route change cross-fades it while the frame around it stays put; wrap what changes between pages, once per page, or give each a `name` (a CSS identifier) when there are two; it fades when the navigation runs in a view transition, through `startPageTransition(update)` (flushes a synchronous state change inside `document.startViewTransition`) or a router's own option such as TanStack Router `viewTransition`, and otherwise, in a browser without View Transitions, or under `prefers-reduced-motion: reduce`, it swaps with no fade; it renders a small `<style>`, with no colour in it, and the div is a stacking context, so portal an overlay that must rise above the frame). `EmptyState` (what a page or list shows when it has nothing yet: a centred `title`, the `children` as the explanation under it, and `actions` in a centred row; a named `<section>`; `level` is the title's heading level, 2 (default) to 6, and does not change its look; for one muted line inside a card, `Text as="p" color="muted"`). `Steps` (a numbered `<ol>` of `Step`s, each a `title` and the `children` as a detail line, the numbers counted by CSS in a violet disc; for a "get started" list, not for progress). `Divider` (a hairline `<hr>` in the theme's line colour; spacing comes from the layout around it). `BulletList` (a `<ul>` with discs, an indent and a small gap; children are `<li>`s). `Code` (inline code: mono, on a subtle background). `CodeBlock` (a block of code, no client JS: the text is `children` (a string) or `lines` (one entry per line), `language` (`bash`, `css`, `tsx`, `ts` or `json`) colours it and `text`, the default, leaves it in one colour; `label` is a caption naming the language or file; long lines wrap by default, so nothing is cut off and a command copies as one line, and `overflow="scroll"` keeps lines whole and scrolls them under a thin scrollbar in the theme's muted ink; `copy` adds a copy button beside the code, which copies it exactly as given and needs the page hydrated; other props go on the root `<figure>`). `CodeExample` (one framed example: the rendered `children` over the `code` that makes them, coloured as TSX, in one bordered box, with an optional `caption` as its last band; a wide preview or long code scrolls inside the frame under the same thin scrollbar; `copy` adds a button that copies the code; no client JS without `copy`). `Video` (a full-width, rounded `<video>` with controls that loads metadata only; `src` is required, `poster` and a WebVTT `captions` track are optional, children are the fallback). `Kbd` (a keyboard key in mono; its border takes the surrounding text colour, so it reads on a filled button too). `FilterGroup` (a `<fieldset>` with a small uppercase `legend` over a wrapped row of its children, e.g. `outline` `sm` `Button`s with `pressed`). `DateField` (see Dates).
83
+ - Controls: `Button` (`variant` is `primary` (default), `outline`, `ghost`, `link` or `dashed`, the link having no box: text with an underline on hover, for an in-line action, and the dashed one a dashed outline in muted text that fills its container, like `Card dashed`, for an "add another" row, with a leading icon as a child, ignoring `tone`; `size="sm"` is compact, `text-xs`, for table rows and toolbars, `size="lg"` is `text-base` with more padding; `tone="danger"` is red, for delete and clear, and `tone="positive"` green, for accept and approve; `disabledReason` disables it, greys it flat and makes the reason its `title`; `busy` disables it with `aria-busy` and keeps its colour, showing `busyLabel` in place of the children; `pressed` makes it a toggle with `aria-pressed`, on `outline` and `ghost` neutral when off and filled in the tone's primary colour when on, so an `outline` `size="sm"` Button with `pressed` is a filter chip; `count` puts a small number chip after the label, part of the button's name ("Splits, 3"), and a button that opens something also takes `aria-expanded`; with `href`, or `render` to draw the app's own router `<Link>`, it is a link with the same look, `external` opening a new tab), `ButtonGroup` (a wrapping row of buttons; `end` is one item pushed to the far side, `label` makes it a named `role="group"`), `IconButton` (a square button holding one icon; `label` is required and becomes its `aria-label` and `title`; `variant` is `outline` (default) or `ghost`, `tone` is `default` or `danger`, `size` matches a `Button`'s height; `badge` puts a red count on its corner, not shown at 0; with `href` or `render` it is a link, `external` opening a new tab), `Icon` (one bare `<svg>` sized to fill a box, or `name` for one of the set's own icons (`diff`, `baseline`, `review`, `ci`, `self-host`, `privacy`, `capture`, `branch`, `theme`, `speed`: one 24 grid, a 1.5 stroke, round caps and joins, each the idea and not a product): `size` `sm` 14px, `md` 16px (default), `lg` 20px or `feature` 32px; `tone` is a text tone or `inherit` (default, the text colour around it); `aria-hidden` unless `label` names it as `role="img"`), `ColourSchemeToggle` (a light/dark switch on `<html>`; `id`, `data-*` and other button props go on the button, `store={false}` skips `localStorage`; see Light and dark), `Input` (a text input; `icon` adds a leading icon inside the field; `prefix` is short text inside the border before the value, a currency symbol, on a plain field only; `type="search"` drops the browser's search look and shows a "Clear search" button while the field has a value), `Textarea` (a multi-line text input, `rows` defaults to 3), `Select` (a native `<select>` in the control look; takes `<option>` children and keeps the browser's arrow). `Input`, `Select` and `Textarea` take `size` (`md` default, `sm`: `text-xs`, less padding, for dense forms and table rows, or `lg`: `text-base`, more padding) and `width` (`full` default, the fixed `xs` 5rem, `sm` 7rem, `md` 9rem, `lg` 12rem, `fill`: grows to the room left in a flex row and can shrink to nothing, so the buttons beside it stay on the line, or `auto`: no width class, as wide as the browser makes it); a fixed width replaces `w-full`, so there is no class to collide with. `Input` also takes `align` (`start` default, or `end` to right-align a numeric value). `size` replaces the native `size` attribute, so a multi-row `<select size={4}>` is not reachable through `Select`. `Checkbox` (a native checkbox with its label; other props go on the input; `size="sm"` is `text-xs` with a smaller box and `size="lg"` `text-base` with a larger one, `align="start"` puts the box on the first line of a label that wraps, where the default `center` centres it on all of them, and with no `label` it is a bare box for a table cell, named by `aria-label`; `description` is a muted line under the label, the box's `aria-describedby`). `Switch` (an on/off setting that acts at once, usually by saving to a server: a native `<button role="switch" aria-checked>`, controlled only, with `checked` and `onChange(next)`; give it a `label` (the whole label is the click target) or, with none, an `aria-label`: one is required; `description` is a muted line under the label and its `aria-describedby`; `size` is `sm`, `md` (default) or `lg`; `busy` is the save in flight: `aria-busy`, a click or key does nothing, it keeps its colour and its focus; `disabled` is the native one, washed out; a value sent with a form stays a `Checkbox`). `RadioGroup` and `Radio` (one choice from a few options: the group has a `label`, `value`/`onChange` or `defaultValue`, `name`, `isDisabled`, `isRequired`, `size` (`sm`, `md` default, `lg`, as `Checkbox`) and an `error` shown like `Field`'s with `role="alert"`; each `Radio` has a `value`, a `label`, an optional `description` and `isDisabled`; one tab stop, the arrow keys move and choose). `Disclosure` (a summary button with a chevron and `aria-expanded` over a body that opens and closes; `summary`, `children`, `defaultOpen`; it keeps its own state and replaces `<details>`). `FileDrop` (a dashed drop zone, a `<label>` around a file input that covers it, so a click opens the dialog and a dropped file lands on the input; `children` is the prompt, `accept`, `multiple`, `name`, `disabled` and `onChange` on the input). `CopyButton` (a small text button that writes `text` to the clipboard and swaps `label` ("copy") for `copiedLabel` ("copied"); every other button prop, `disabled` included, goes on the `<button>`). `CopyLine` (one line of mono text in a box with a `CopyButton` beside it; `disabled` disables the button; other props go on the root `<div>`). `Field` (a label above a control, `htmlFor` names the control's id; `hideLabel` keeps the label for assistive tech and draws no text, for a control whose purpose is plain from what surrounds it, and makes `layout="row"` a column; a `hint` sits under it, and an `error` replaces the hint with a `role="alert"` line; `span="full"` runs it across both columns of a `FormGrid`). `FormGrid` (two columns of `Field`s from `md`, one below it), `FormRow` (an inline-edit row: `FormRowField`s side by side, an `action` after them, and one `hint` or `error` under the whole row; see Layout) and `FormRowField` (a compact label over one control, `span` 1 to 4). `Callout` (a tinted notice box; `tone` is `neutral`, `info` (default), `positive`, `warn` or `danger`; an optional `title` sits in bold above the body; `actions` puts a button or link at the end of the row; `banner` makes it a flat strip, bottom border only and no radius, for the top of a `Card flush`). `ListRow` (a full-width row that is a `<button>`, or a link with `href` or `render`, with a hover fill, a left-aligned label and `trailing` content; `divided` draws a hairline above the row, to separate rows in a `Card flush`; `expanded` makes it the header of a collapsible section: `aria-expanded` and a chevron, the caller owning the state and the section; `leading` is content before the label (an `Avatar`), and `leadingWidth="thumbnail"` gives it a fixed width so rows with and without a picture line up; `size` sets the padding (`sm` dense, `md` default, `lg`); `focused` draws the focus ring on the row the keyboard cursor is on, for a list moved with the arrow keys while focus stays on the list; `selected` marks the row in view with a violet fill and `aria-current="true"`; a link row (`href` or `render`) can hold its own link, button or control in `trailing`, which sits above the row's link and takes its own click; `static` makes it a plain `<div>`, for a row that holds its own buttons where the row would otherwise be a `<button>`, and adds `actions`, buttons after `trailing`, and `revealOnHover`, which shows them only on hover or focus inside the row and always on a touch screen; a compact bordered list is `Card flush` around `static` rows at `size="sm"`, `divided` after the first (no separate part); `active` marks the row the pointer or the keyboard is on, with the hover fill and the `revealOnHover` actions shown, for a list that follows both through the row's `onPointerEnter`, `onPointerLeave` and `onFocus`, which every row passes through). `Tabs` (underline tabs for switching a view in place: a row over a bottom rule, the current tab underlined in the accent; `items` are `{ value, label }` buttons, with `value` the current one's `value` and `onChange(value)` the caller's state, one tab stop and the arrow keys, Home and End moving between enabled tabs, or `{ label, href | render, active?, external? }` links in a `<nav>` with `aria-current="page"` on the current one (`active`, or the item whose `value` is `value`); `label` names the group; a button tab takes `disabled` and `controls`, the id of its `role="tabpanel"`, which is the caller's; `variant="pill"` fills the current tab, with no rule, for `PageHeader`'s `tabs` slot, an `AppShell` `nav` or a rail; only a pill row takes `size` and `orientation`, and `orientation="vertical"` stacks it in one column, the content of a `SidebarLayout` rail; `disabled` on the row disables every tab, e.g. until hydration). `SegmentedControl` (a joined pill group, one option filled; the same `items`, `value`, `onChange`, `disabled` and `label` as `Tabs`, as a `radiogroup` with `aria-checked` on each `radio`). On both an item takes `data-*` attributes (typed `data-${string}`) onto its button or link; on `Tabs` and `SegmentedControl` an item takes `count` (a small muted number after the label, part of its name: "Shots 12") and `title`; a `disabled` button option stays in the page as `aria-disabled`, so its `title` is still the hover tooltip and is announced as the reason, it is out of the tab order and the arrow keys, Home and End skip it. `Dialog` (a modal on the browser's `<dialog>`, opened on mount, so render it only while open: `title` is its name, `description` a line under it, `footer` the right-aligned actions; Esc, the backdrop or a `<form method="dialog">` call `onClose`; `size` is `sm` (320px, a list), `md` (440px, default, a form), `lg` (640px, a wider form or a table) or `full` (the whole screen, for media or a player: the body fills the height between the title and the footer and scrolls if taller; there is no backdrop to click, so put a close button on a `<form method="dialog">` in the `footer`)). `FieldDialog` (a `Dialog` with one text field and Cancel and submit buttons, every string passed in: `title`, `description`, `label`, `inputLabel`, `placeholder`, `hint`, `emptyReason`, `submit` and `busyLabel`; `onSubmit` gets the trimmed value and a resolved promise closes it, a rejection shows its message). `Drawer` (a modal slide-over on the right edge, on a native `<dialog>` opened on mount like `Dialog`, so render it only while open: `title` is its name, a close button sits beside it, `children` scroll between the header and an optional pinned `footer`; Esc, the scrim and the close button call `onClose`; `size` is `sm`, `md` (default) or `lg`; `closeLabel` names the close button). `Breadcrumbs` (a `<nav aria-label="Breadcrumb">` over an ordered list; `crumbs` are nodes, or `{ label, href | render, external? }` for one that links; the last is the current page and is never a link and gets `aria-current="page"`, and each crumb truncates). `Table` (a list of records in a rounded, bordered card around a full-width `<table>` that scrolls sideways on a narrow screen; `caption` names it for assistive tech and is not drawn; compose it from `TableHead`, `TableBody`, `TableFoot`, `TableRow`, `TableHeaderCell` (`align` `start`, `center` or `end`; `srLabel` is a visually hidden column name, in place of children, for a column of row actions: "Actions") and `TableCell` (`align`, `indent` of 1 to 3 to set a start-aligned cell in, for a line under a parent row, `tone` `default`, `strong` or `muted`, or the Callout tones `neutral`, `info`, `positive`, `warn` or `danger` for an amount that means something, in the tone's text colour and with no fill, so pair the colour with a sign or a word; `numeric` for tabular figures, `mono` for a small mono face, `truncate` to cap a long value's width, cut it with an ellipsis and put the full text in a `title`, so its content is text (strings and numbers); `secondary` for a second, muted line under the content; `colSpan` passes through, so a `TableCell` with `colSpan` in a row of its own is a full-width panel, an empty state or a note, centred with `align="center"`); `TableEditCell` (a `TableCell` whose value opens an editor in place: `children` is the value, drawn as a button; a click, Enter or Space swaps it for `renderEditor(close)`, a plain control in the cell and not a popover of its own, and moves the focus into it; `close` and Esc put the focus back on the value, and a press outside or Tab out of it closes it too, so the control saves on its own change or submit; `label` names the button and the editor; it takes the `TableCell` props except `truncate`); `InlineEdit` (the same edit trigger without the cell, for a `TableCell` that holds other content too: `label`, `renderEditor(close)` and `children`, with the same keyboard and focus behaviour); `TableFoot` (the `<tfoot>` for totals: its cells are strong by default and each row has a firmer line above it, in a card and in a `plain` table alike); `TableGroupRow` (a full-width group header in the body: `label` spans `colSpan` columns, the table's column count, which is required; optional `trailing` content sits at the far end, and the group's rows follow it); `tone` on a `TableRow` in the body marks the row with a stripe on its leading edge over a soft fill: `neutral`, `info`, `positive`, `warn` or `danger`, as `Callout`, or `muted`, which only fades the row (a transfer, a row that is not the point); a bold subtotal is not a tone: `emphasis` on a `TableRow` in the body gives it a `TableFoot` row's look among the other rows; `stripe` (a text tone) draws the leading stripe alone, with no fill; a row takes one of `tone`, `stripe` and `emphasis`; `variant="plain"` on `Table` drops the card border, fill and shadow, the head strip and the row hover and keeps the row dividers, for a table inside a card of its own; every table element passes `id`, `data-*` and `aria-*` through, and none takes a `className` or `style`). `Timeline` (a named `<ol>` of events; `label` is its `aria-label`, `empty` is drawn instead when there are no items) with `TimelineItem` (a `title`, an already-formatted `time`, the `children` as a note, and `tone`, a Callout tone for the marker dot, `neutral` by default). `KeyValueList` (a `<dl>` of label/value rows; each `KeyValue` takes a `label` and its value as children, the value right-aligned in tabular figures; `total` makes it the sum row, a firm rule above and strong text, and `tone` (`muted`, `positive`, `warn`, `danger`, `info`) colours the value on any row, so `total tone="warn"` is a total that does not reconcile; `align="start"` puts the values left-aligned in one column beside the labels instead, using CSS subgrid: in Chrome before 117 a row stacks its label over its value). `Avatar` (a round badge of the initials of `name`, titled with the name and `aria-hidden`; with `src` (the app's own local image path, never a remote one) it is a round, cropped `<img alt={name}>` instead, and if that image fails to load it shows the initials; `size` is `xs` (16px, for an inline mention), `sm`, `md` (default), `lg`, `xl` (56px), `2xl` (64px) or `3xl` (96px); `onClick` makes it one button named by `name`; `ring` adds a page-colour border and a negative margin for an overlapping stack; the colour is one of seven stock background classes picked by a hash of the name, remapped by each theme, and `avatarColourClass(name)` returns it to colour something else the same way; `initialsOf(name)` is the two letters). `TextLink` (an inline text link in the accent colour; `href` is required unless `render` draws the app's own router `<Link>`, `external` adds `target="_blank"` and `rel="noreferrer"`, `stretch` covers the nearest `relative` ancestor so a whole row or card is the click target, `size` (`sm`, `md` or `lg`) pins the type size where it would otherwise follow the text around it, `mono` sets the monospace face, `tone` (`warn`, `danger`, `positive`, `info` or `muted`) gives it a meaning's colour in place of the accent, every other anchor prop passes through). `MetaList` (a wrapping row of facts or links in muted small text, joined by an `aria-hidden` `separator`: `·` (default), `•` or `none`; branch · SHA · date, or a footer's Privacy · Terms; an item is text, a `Code` or a `TextLink`; `nowrap` keeps each item on one line, for links; `mono` sets the row in the monospace face, for ids and paths). `PageFade` (the page's content area, named for the View Transitions API so a route change cross-fades it while the frame around it stays put; wrap what changes between pages, once per page, or give each a `name` (a CSS identifier) when there are two; it fades when the navigation runs in a view transition, through `startPageTransition(update)` (flushes a synchronous state change inside `document.startViewTransition`) or a router's own option such as TanStack Router `viewTransition`, and otherwise, in a browser without View Transitions, or under `prefers-reduced-motion: reduce`, it swaps with no fade; it renders a small `<style>`, with no colour in it, and the div is a stacking context, so portal an overlay that must rise above the frame). `EmptyState` (what a page or list shows when it has nothing yet: a centred `title`, the `children` as the explanation under it, and `actions` in a centred row; a named `<section>`; `level` is the title's heading level, 2 (default) to 6, and does not change its look; for one muted line inside a card, `Text as="p" color="muted"`). `Steps` (a numbered `<ol>` of `Step`s, each a `title` and the `children` as a detail line, the numbers counted by CSS in a violet disc; for a "get started" list, not for progress). `Divider` (a hairline `<hr>` in the theme's line colour; spacing comes from the layout around it). `BulletList` (a `<ul>` with discs, an indent and a small gap; children are `<li>`s). `Code` (inline code: mono, on a subtle background). `CodeBlock` (a block of code, no client JS: the text is `children` (a string) or `lines` (one entry per line), `language` (`bash`, `css`, `tsx`, `ts` or `json`) colours it and `text`, the default, leaves it in one colour; `label` is a caption naming the language or file; long lines wrap by default, so nothing is cut off and a command copies as one line, and `overflow="scroll"` keeps lines whole and scrolls them under a thin scrollbar in the theme's muted ink; `copy` adds a copy button beside the code, which copies it exactly as given and needs the page hydrated; other props go on the root `<figure>`). `CodeExample` (one framed example: the rendered `children` over the `code` that makes them, coloured as TSX, in one bordered box, with an optional `caption` as its last band; a wide preview or long code scrolls inside the frame under the same thin scrollbar; `copy` adds a button that copies the code; no client JS without `copy`). `Video` (a full-width, rounded `<video>` with controls that loads metadata only; `src` is required, `poster` and a WebVTT `captions` track are optional, children are the fallback). `Kbd` (a keyboard key in mono; its border takes the surrounding text colour, so it reads on a filled button too). `FilterGroup` (a `<fieldset>` with a small uppercase `legend` over a wrapped row of its children, e.g. `outline` `sm` `Button`s with `pressed`). `DateField` (see Dates).
84
84
  - Dates: `toISODate`, `fromISODate`, and the `CalendarDate` type.
85
85
 
86
86
  ## Dates
@@ -253,6 +253,11 @@ except the last four.
253
253
  `children` is the list, replaced by `empty` when `isEmpty`.
254
254
  - **`DetailLayout`**: use this when a page is one thing with facts beside it. `children` take two
255
255
  thirds from `lg` and `aside` one third; below `lg` the aside stacks under the main column.
256
+ `stickyAside` (off by default, since an aside of ordinary cards may be taller than the screen)
257
+ keeps the aside in view from `lg` as a long page scrolls: it sticks under the shell's header and
258
+ scrolls itself when it is taller than the room left.
259
+ `asideLabel` names the aside's landmark (it never folds it), for a page with another complementary
260
+ landmark.
256
261
  - **`FormLayout`**: use this for a create or edit page. A `max-w-3xl` `<form>` that starts at the
257
262
  Container's left edge (it is not re-centred, so the title does not jump), with an `actions` row at
258
263
  the end. Other props (`onSubmit`, `action`) go on the `<form>`.
@@ -271,14 +276,27 @@ except the last four.
271
276
  plain `<AppShell>`, no `bleed`: it sits in the Container like every other layout, so the rail
272
277
  starts at the header logo's x and the canvas ends at the header's right edge. The rail has no fill
273
278
  and no border (pill `Tabs` with `orientation="vertical"` is its content); from `md` it is `md:w-56` beside
274
- the canvas, and below `md` it stacks above it. A long rail sets `railLabel`: below `md` the rail
275
- then folds, closed, behind a disclosure of that name, so a phone shows the page first.
279
+ the canvas, and below `md` it stacks above it. From `md` the rail also stays in view under the
280
+ shell's header as a long page scrolls (always, with or without `railLabel`); a rail taller than
281
+ the screen scrolls itself, so its last link is reachable. A long rail sets `railLabel`: below `md`
282
+ the rail then folds, closed, behind a disclosure of that name, so a phone shows the page first;
283
+ `railLabel` also names the rail's `<aside>` landmark.
284
+ - **`SideColumn`**: use this when part of a page is a main area beside a side column of a known width,
285
+ such as a thread beside an assistant. `children` are the main area and `aside` the column; from `lg`
286
+ the aside is `asideWidth` wide on the right (`sm` 18rem, `md` 24rem, the default) and the main area
287
+ takes the room left, and below `lg` the aside stacks under it. `asideLabel` folds the aside, closed,
288
+ behind a native disclosure of that name below `lg` (the aside stays mounted, so state inside it is
289
+ kept) and names the `<aside>` landmark; from `lg` it is the same column, always shown. It sets no page width, padding or header of
290
+ its own, so it sits inside a layout or a card; a share of a page under a `PageHeader` is
291
+ `DetailLayout`'s aside. A `Stack` with `rowFrom` and a `ClusterGrow` sizes the side by its content
292
+ and is not a fixed column. A grid of fields, one row per item, is a `plain` `Table` in a `Card`.
276
293
  - **`FullBleedLayout`**: use this for a viewer that needs the whole width. Render it in
277
294
  `<AppShell bleed>`; `children` fill the width, and an optional `panel` takes `lg:w-72` on the
278
295
  right and stacks below under `lg`. It adds no padding, so a viewer fills the width; set `padded`
279
296
  for a body that is not a viewer, which gives the canvas the other layouts' page padding. A `bar`
280
297
  (a viewer's title and actions) runs full width above both on the surface with a bottom line, and
281
- sticks under the shell's header as the page scrolls; `padded` does not pad it.
298
+ sticks under the shell's header as the page scrolls; `padded` does not pad it. `panelLabel` names
299
+ the panel's `<aside>` landmark.
282
300
 
283
301
  **`FullBleedLayout` deliberately breaks the shared left edge from `lg` up.** An edge-to-edge
284
302
  viewer needs the viewport's edges, so it cannot sit inside the Container the header uses. At
@@ -332,14 +350,16 @@ Two states, light and dark. The stored choice wins; with nothing stored the syst
332
350
  <ColourSchemeToggle onChange={(scheme) => track(scheme)} />;
333
351
  ```
334
352
 
335
- It is an `IconButton` showing the scheme you would switch *to* (a sun on a dark page, a moon on a
353
+ It is an icon button showing the scheme you would switch *to* (a sun on a dark page, a moon on a
336
354
  light one), labelled "Switch to light theme" / "Switch to dark theme". A click applies the scheme
337
355
  to `<html>` and stores it under `localStorage["hackcove:colour-scheme"]`; a storage error (private
338
356
  mode) is swallowed and the page still switches. It reads the current state from the `dark` class
339
357
  when it mounts, so it agrees with the script and renders the same on the server and the client.
340
358
  It draws both icons and a small `<style>` shows one from the class on `<html>`, so a dark visitor
341
359
  with no cookie and no `initial` sees the sun from the first frame, not the server's light-page moon;
342
- the button's name catches up on hydration.
360
+ the button's name is drawn the same way (both names are in the markup, visually hidden, and the
361
+ rule hides the wrong one), so a screen reader hears the right one before hydration too, with no
362
+ script. Only the tooltip waits for hydration.
343
363
 
344
364
  4. **A light theme with a class of its own, or a scheme the server knows.** An app that picks the
345
365
  scheme from a cookie and whose light theme has a scope class passes both:
package/dist/Avatar.d.ts CHANGED
@@ -6,7 +6,8 @@ declare const SIZE_CLASSES: {
6
6
  readonly md: "size-8 text-xs";
7
7
  readonly lg: "size-10 text-base";
8
8
  readonly xl: "size-14 text-xl";
9
- readonly "2xl": "size-24 text-3xl";
9
+ readonly "2xl": "size-16 text-2xl";
10
+ readonly "3xl": "size-24 text-3xl";
10
11
  };
11
12
  export type AvatarSize = keyof typeof SIZE_CLASSES;
12
13
  /** `round` for a person, `square` (rounded corners that grow with the size) for an org or a team. */
@@ -28,7 +29,7 @@ type AvatarProps = {
28
29
  */
29
30
  src?: string | undefined;
30
31
  /**
31
- * `xs` 16px, `sm` 24px, `md` 32px, `lg` 40px, `xl` 56px or `2xl` 96px.
32
+ * `xs` 16px, `sm` 24px, `md` 32px, `lg` 40px, `xl` 56px, `2xl` 64px or `3xl` 96px.
32
33
  * @default md
33
34
  */
34
35
  size?: AvatarSize;
@@ -38,16 +39,22 @@ type AvatarProps = {
38
39
  * @default round
39
40
  */
40
41
  shape?: AvatarShape;
41
- } & Unstyled<Omit<HTMLAttributes<HTMLElement>, "children">>;
42
+ /**
43
+ * Makes the Avatar one button, named by `name`: a person who opens their profile or a menu.
44
+ * Without it the Avatar is a plain mark.
45
+ */
46
+ onClick?: (() => void) | undefined;
47
+ } & Unstyled<Omit<HTMLAttributes<HTMLElement>, "children" | "onClick">>;
42
48
  /**
43
49
  * A person as a round badge of initials, or as their photo when `src` is given; `shape="square"`
44
50
  * marks an org or a team instead. It never loads a remote image on its own: that is a request to
45
51
  * another server for decoration, and it makes a screenshot depend on the network. `src` is the
46
- * app's own local path. A row of overlapping avatars is an `AvatarStack`.
52
+ * app's own local path. With `onClick` the whole Avatar is one button. A row of overlapping avatars
53
+ * is an `AvatarStack`.
47
54
  */
48
55
  export declare function Avatar(props: AvatarProps): ReactElement;
49
56
  /** Avatar plus the stack's ring: internal, so the ring only ever comes from `AvatarStack`. */
50
- export declare function AvatarMark({ name, src, size, shape, ring, onError, ...rest }: AvatarProps & {
57
+ export declare function AvatarMark({ name, src, size, shape, ring, onError, onClick, ...rest }: AvatarProps & {
51
58
  ring?: string | false;
52
59
  }): ReactElement;
53
60
  export {};
package/dist/Avatar.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useState } from "react";
3
- import { cx } from "./cx.js";
3
+ import { cx, focusRing } from "./cx.js";
4
4
  /** The identity colours, in hash order. Each is a stock background utility; a theme remaps them. */
5
5
  const COLOUR_CLASSES = [
6
6
  "bg-violet-600",
@@ -17,7 +17,8 @@ const SIZE_CLASSES = {
17
17
  md: "size-8 text-xs",
18
18
  lg: "size-10 text-base",
19
19
  xl: "size-14 text-xl",
20
- "2xl": "size-24 text-3xl",
20
+ "2xl": "size-16 text-2xl",
21
+ "3xl": "size-24 text-3xl",
21
22
  };
22
23
  const SQUARE_CLASSES = {
23
24
  xs: "rounded-sm",
@@ -25,7 +26,8 @@ const SQUARE_CLASSES = {
25
26
  md: "rounded-md",
26
27
  lg: "rounded-md",
27
28
  xl: "rounded-lg",
28
- "2xl": "rounded-xl",
29
+ "2xl": "rounded-lg",
30
+ "3xl": "rounded-xl",
29
31
  };
30
32
  /** The corner classes for a shape at a size; `AvatarStack`'s "+N" chip uses the same. */
31
33
  export function avatarShapeClass(shape, size) {
@@ -53,21 +55,24 @@ export function initialsOf(name) {
53
55
  * A person as a round badge of initials, or as their photo when `src` is given; `shape="square"`
54
56
  * marks an org or a team instead. It never loads a remote image on its own: that is a request to
55
57
  * another server for decoration, and it makes a screenshot depend on the network. `src` is the
56
- * app's own local path. A row of overlapping avatars is an `AvatarStack`.
58
+ * app's own local path. With `onClick` the whole Avatar is one button. A row of overlapping avatars
59
+ * is an `AvatarStack`.
57
60
  */
58
61
  export function Avatar(props) {
59
62
  return _jsx(AvatarMark, { ...props });
60
63
  }
61
64
  /** Avatar plus the stack's ring: internal, so the ring only ever comes from `AvatarStack`. */
62
- export function AvatarMark({ name, src, size = "md", shape = "round", ring = false, onError, ...rest }) {
65
+ export function AvatarMark({ name, src, size = "md", shape = "round", ring = false, onError, onClick, ...rest }) {
63
66
  // The `src` that failed to load: a new `src` gets its own try, so a broken one never sticks.
64
67
  const [failedSrc, setFailedSrc] = useState();
65
68
  const corners = avatarShapeClass(shape, size);
66
- if (src && src !== failedSrc) {
67
- return (_jsx("img", { src: src, alt: name, title: name, className: cx("shrink-0 object-cover", corners, SIZE_CLASSES[size], ring), onError: (event) => {
68
- onError?.(event);
69
- setFailedSrc(src);
70
- }, ...rest }));
71
- }
72
- return (_jsx("span", { "aria-hidden": "true", title: name, className: cx("inline-grid shrink-0 place-items-center font-semibold text-violet-50", corners, SIZE_CLASSES[size], avatarColourClass(name), ring), ...rest, children: initialsOf(name) }));
69
+ // As a button, the button takes the consumer's props and the mark inside keeps only its look.
70
+ const markProps = onClick ? {} : rest;
71
+ const mark = src && src !== failedSrc ? (_jsx("img", { src: src, alt: name, title: name, className: cx("shrink-0 object-cover", corners, SIZE_CLASSES[size], ring), onError: (event) => {
72
+ onError?.(event);
73
+ setFailedSrc(src);
74
+ }, ...markProps })) : (_jsx("span", { "aria-hidden": "true", title: name, className: cx("inline-grid shrink-0 place-items-center font-semibold text-violet-50", corners, SIZE_CLASSES[size], avatarColourClass(name), ring), ...markProps, children: initialsOf(name) }));
75
+ if (!onClick)
76
+ return mark;
77
+ return (_jsx("button", { "aria-label": name, ...rest, type: "button", onClick: onClick, className: cx("inline-grid shrink-0 cursor-pointer", corners, focusRing), children: mark }));
73
78
  }
@@ -8,7 +8,8 @@ const OVERLAP = {
8
8
  md: "-ml-2",
9
9
  lg: "-ml-2.5",
10
10
  xl: "-ml-3.5",
11
- "2xl": "-ml-6",
11
+ "2xl": "-ml-4",
12
+ "3xl": "-ml-6",
12
13
  };
13
14
  // The ring in the page colour that keeps two overlapping avatars apart.
14
15
  const RING = "border-2 border-slate-50";
package/dist/Button.d.ts CHANGED
@@ -40,6 +40,12 @@ type ButtonAsButton = ButtonLook & {
40
40
  * Left out, the button is not a toggle, has no `aria-pressed` and keeps its variant's look.
41
41
  */
42
42
  pressed?: boolean | undefined;
43
+ /**
44
+ * A small number in a chip after the label: how many are behind the button (the splits a row
45
+ * has, the comments on a thread). It is part of the button's name, "Splits, 3", and `0` is
46
+ * shown. For a button that opens something, also pass `aria-expanded`.
47
+ */
48
+ count?: number | undefined;
43
49
  href?: undefined;
44
50
  render?: undefined;
45
51
  external?: undefined;
@@ -51,6 +57,12 @@ type ButtonAsLink = ButtonLook & {
51
57
  busy?: undefined;
52
58
  busyLabel?: undefined;
53
59
  pressed?: undefined;
60
+ /**
61
+ * A small number in a chip after the label: how many are behind the button (the splits a row
62
+ * has, the comments on a thread). It is part of the button's name, "Splits, 3", and `0` is
63
+ * shown. For a button that opens something, also pass `aria-expanded`.
64
+ */
65
+ count?: number | undefined;
54
66
  } & Unstyled<Omit<ComponentPropsWithoutRef<"a">, "href">> & ({
55
67
  /** Makes the button a link to this URL. */
56
68
  href: string;
@@ -68,7 +80,9 @@ type ButtonProps = ButtonAsButton | ButtonAsLink;
68
80
  * underline on hover, for an in-line action; `dashed` is the full-width "add" row, a dashed
69
81
  * outline with muted text that fills its container, with an optional leading icon in the
70
82
  * children). `size="sm"` is compact and `size="lg"` larger; `tone="positive"` is green and `tone="danger"` is red;
71
- * `pressed` makes it a toggle: neutral when off, the tone's fill when on.
83
+ * `pressed` makes it a toggle: neutral when off, the tone's fill when on. `count` puts a small
84
+ * number chip after the label; the chip is part of the name, and a button that opens something
85
+ * also takes `aria-expanded`.
72
86
  * Defaults to `type="button"`. `disabledReason` and `busy` are the two disabled states that say
73
87
  * more than a plain `disabled`, which keeps its half-opacity look. `href` makes it a link instead,
74
88
  * with the same look (a plain `<a>`, or with `render` the app's own router `<Link>`:
package/dist/Button.js CHANGED
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cx, focusRing } from "./cx.js";
3
3
  import { renderLink } from "./link.js";
4
4
  const SHAPE = "inline-flex items-center justify-center rounded-lg border font-medium transition";
@@ -55,6 +55,15 @@ const TOGGLE_OFF = {
55
55
  color: "border-slate-200 bg-white text-slate-600",
56
56
  hover: "hover:border-violet-400",
57
57
  };
58
+ // The count chip: a pill in the button's own text colour (a hairline of `currentColor`), so it reads
59
+ // on every variant and tone and needs no palette entry. The size is a Badge's.
60
+ const COUNT = "inline-flex h-4 min-w-4 items-center justify-center rounded-full border border-current px-1 text-[10px] font-semibold leading-none tabular-nums";
61
+ /** The label, then the count chip. The comma is for a screen reader only, so the name reads "Splits, 3". */
62
+ function withCount(children, count) {
63
+ if (count === undefined)
64
+ return children;
65
+ return (_jsxs(_Fragment, { children: [children, _jsx("span", { className: "sr-only", children: ", " }), _jsx("span", { className: COUNT, children: count })] }));
66
+ }
58
67
  function lookOf(variant, tone, pressed) {
59
68
  if (variant === "dashed")
60
69
  return DASHED;
@@ -107,7 +116,9 @@ function stateClass({ variant = "primary", size = "md", tone = "default" }, stat
107
116
  * underline on hover, for an in-line action; `dashed` is the full-width "add" row, a dashed
108
117
  * outline with muted text that fills its container, with an optional leading icon in the
109
118
  * children). `size="sm"` is compact and `size="lg"` larger; `tone="positive"` is green and `tone="danger"` is red;
110
- * `pressed` makes it a toggle: neutral when off, the tone's fill when on.
119
+ * `pressed` makes it a toggle: neutral when off, the tone's fill when on. `count` puts a small
120
+ * number chip after the label; the chip is part of the name, and a button that opens something
121
+ * also takes `aria-expanded`.
111
122
  * Defaults to `type="button"`. `disabledReason` and `busy` are the two disabled states that say
112
123
  * more than a plain `disabled`, which keeps its half-opacity look. `href` makes it a link instead,
113
124
  * with the same look (a plain `<a>`, or with `render` the app's own router `<Link>`:
@@ -116,18 +127,18 @@ function stateClass({ variant = "primary", size = "md", tone = "default" }, stat
116
127
  export function Button(props) {
117
128
  return isLink(props) ? _jsx(LinkButton, { ...props }) : _jsx(PlainButton, { ...props });
118
129
  }
119
- function LinkButton({ variant = "primary", size = "md", tone = "default", href, render, external = false, children, ...rest }) {
130
+ function LinkButton({ variant = "primary", size = "md", tone = "default", href, render, external = false, count, children, ...rest }) {
120
131
  return renderLink({
121
132
  ...rest,
122
133
  href,
123
134
  render,
124
135
  external,
125
136
  className: buttonClass(variant, { size, tone }),
126
- children,
137
+ children: withCount(children, count),
127
138
  });
128
139
  }
129
- function PlainButton({ variant, size, tone, disabledReason, busy = false, busyLabel, pressed, children, title, disabled, ...rest }) {
140
+ function PlainButton({ variant, size, tone, disabledReason, busy = false, busyLabel, pressed, count, children, title, disabled, ...rest }) {
130
141
  const state = stateOf(busy, disabledReason);
131
142
  const blocked = state === "blocked";
132
- return (_jsx("button", { type: "button", className: stateClass({ variant, size, tone }, state, pressed), disabled: disabled || busy || blocked, "aria-busy": busy || undefined, "aria-pressed": pressed, title: blocked ? disabledReason : title, ...rest, children: busy && busyLabel ? busyLabel : children }));
143
+ return (_jsx("button", { type: "button", className: stateClass({ variant, size, tone }, state, pressed), disabled: disabled || busy || blocked, "aria-busy": busy || undefined, "aria-pressed": pressed, title: blocked ? disabledReason : title, ...rest, children: busy && busyLabel ? busyLabel : withCount(children, count) }));
133
144
  }
package/dist/Card.d.ts CHANGED
@@ -5,8 +5,15 @@ import { type LinkRender } from "./link.js";
5
5
  import type { Unstyled } from "./props.js";
6
6
  import type { Size } from "./size.js";
7
7
  type CardBase = {
8
- /** The card's heading, a small uppercase `h2`; on a `section` or `article` it names the card. */
8
+ /** The card's heading, an `h2` (a small uppercase label unless `titleStyle` says otherwise); on a `section` or `article` it names the card. */
9
9
  title?: string;
10
+ /**
11
+ * How the `title` is set. `overline` is the small uppercase label of a panel; `name` is the same
12
+ * `h2` in sentence case and the primary ink, for a card whose title is a name (a shop, a listing,
13
+ * an event). Count, status, actions, `bar` and `size` work the same with both.
14
+ * @default overline
15
+ */
16
+ titleStyle?: "overline" | "name";
10
17
  /** A small number beside the title (how many rows the card holds). Needs a `title`. */
11
18
  count?: number;
12
19
  /** A `Pill` (or any node) directly beside the title and count, not at the far end like `actions`. Needs a `title`. */
package/dist/Card.js CHANGED
@@ -95,7 +95,7 @@ export function Card(props) {
95
95
  }
96
96
  /** A card that is not a link: `as` (a `section` by default) named by its heading. */
97
97
  function PanelCard(props) {
98
- const { title, count, status, actions, footer, bar, flush, dashed, tone, badge, inset, attention, icon, size = "md", children, ...panel } = props;
98
+ const { title, titleStyle, count, status, actions, footer, bar, flush, dashed, tone, badge, inset, attention, icon, size = "md", children, ...panel } = props;
99
99
  const { as: Tag = "section", ...rest } = panel;
100
100
  const headingId = useId();
101
101
  const form = withFooter(cardForm({ flush, bar, dashed, tone, attention, icon, size }, Boolean(title || actions)), footer);
@@ -104,7 +104,7 @@ function PanelCard(props) {
104
104
  Tag !== "div" &&
105
105
  rest["aria-label"] === undefined &&
106
106
  rest["aria-labelledby"] === undefined;
107
- return (_jsxs(Tag, { ...(named ? { "aria-labelledby": headingId } : {}), ...rest, className: cardClass(form), children: [cardBody(form, { title, count, status, actions, inset, children, headingId }), form.footer && _jsx(CardFooter, { size: form.size, children: footer }), badge && _jsx(CardBadge, { size: form.size, children: badge })] }));
107
+ return (_jsxs(Tag, { ...(named ? { "aria-labelledby": headingId } : {}), ...rest, className: cardClass(form), children: [cardBody(form, { title, titleStyle, count, status, actions, inset, children, headingId }), form.footer && _jsx(CardFooter, { size: form.size, children: footer }), badge && _jsx(CardBadge, { size: form.size, children: badge })] }));
108
108
  }
109
109
  /** What names or marks the link itself: these stay on the overlay `<a>`; `id`, `data-*` and every other `aria-*` go on the card. */
110
110
  const LINK_OWN = new Set(["aria-label", "aria-labelledby", "aria-current"]);
@@ -120,7 +120,7 @@ function splitRootAttrs(props) {
120
120
  }
121
121
  /** A card that is a link: a `div` with the link stretched over it as an empty overlay, so the card's own links and buttons are not nested in it. */
122
122
  function LinkCard(props) {
123
- const { title, count, status, actions, bar, flush, dashed, tone, badge, inset, attention, icon, size = "md", children, external = false, ...rest } = props;
123
+ const { title, titleStyle, count, status, actions, bar, flush, dashed, tone, badge, inset, attention, icon, size = "md", children, external = false, ...rest } = props;
124
124
  const form = cardForm({ flush, bar, dashed, tone, attention, icon, size }, Boolean(title));
125
125
  const generated = useId();
126
126
  const [root, link] = splitRootAttrs(rest);
@@ -132,15 +132,24 @@ function LinkCard(props) {
132
132
  external,
133
133
  className: cx("link-stretch absolute inset-0 rounded-xl", focusRing, "focus-visible:-outline-offset-2"),
134
134
  children: null,
135
- }), cardBody(form, { title, count, status, actions, inset, children, headingId: undefined }), badge && _jsx(CardBadge, { size: form.size, children: badge })] }));
135
+ }), cardBody(form, {
136
+ title,
137
+ titleStyle,
138
+ count,
139
+ status,
140
+ actions,
141
+ inset,
142
+ children,
143
+ headingId: undefined,
144
+ }), badge && _jsx(CardBadge, { size: form.size, children: badge })] }));
136
145
  }
137
146
  function insetBand(form, inset) {
138
147
  if (!inset || form.dashed)
139
148
  return null;
140
149
  return (_jsx(CardInset, { size: form.size, bleed: !form.flush, children: inset }));
141
150
  }
142
- function cardBody(form, { title, count, status, actions, inset, children, headingId, }) {
143
- const heading = title && (_jsx(CardHeading, { id: headingId, title: title, count: count, status: status, bar: form.bar, spaced: !form.bar && !actions, size: form.size }));
151
+ function cardBody(form, { title, titleStyle, count, status, actions, inset, children, headingId, }) {
152
+ const heading = title && (_jsx(CardHeading, { id: headingId, title: title, titleStyle: titleStyle, count: count, status: status, bar: form.bar, spaced: !form.bar && !actions, size: form.size }));
144
153
  const grow = form.footer && "flex-1";
145
154
  const band = insetBand(form, inset);
146
155
  if (form.bar) {
@@ -1,9 +1,10 @@
1
1
  import type { ReactNode } from "react";
2
2
  import type { IconName } from "./icons.js";
3
3
  import type { Size } from "./size.js";
4
- export declare function CardHeading({ id, title, count, status, bar, spaced, size, }: {
4
+ export declare function CardHeading({ id, title, titleStyle, count, status, bar, spaced, size, }: {
5
5
  id: string | undefined;
6
6
  title: string;
7
+ titleStyle: "overline" | "name" | undefined;
7
8
  count: number | undefined;
8
9
  status: ReactNode;
9
10
  bar: boolean;
package/dist/CardParts.js CHANGED
@@ -8,9 +8,13 @@ const HEADING_GAP = { sm: "mb-2", md: "mb-3", lg: "mb-4" };
8
8
  const INSET_BLEED = { sm: "-mx-3", md: "-mx-5", lg: "-mx-6" };
9
9
  /** The padding of a card's bar and footer strips, by `size`. */
10
10
  const STRIP_PAD = { sm: "px-3 py-2", md: "px-5 py-3", lg: "px-6 py-4" };
11
- export function CardHeading({ id, title, count, status, bar, spaced, size, }) {
11
+ /** The `h2` of a card whose title is a name: sentence case, in the primary ink. */
12
+ function NameTitle({ id, children }) {
13
+ return (_jsx("h2", { id: id, className: "text-base font-semibold text-slate-900", children: children }));
14
+ }
15
+ export function CardHeading({ id, title, titleStyle, count, status, bar, spaced, size, }) {
12
16
  const ink = bar ? "text-slate-500" : "text-slate-400";
13
- return (_jsxs("div", { className: cx("flex gap-2", status ? "items-center" : "items-baseline", spaced && HEADING_GAP[size]), children: [_jsx(Overline, { as: "h2", id: id, color: bar ? "tertiary" : "muted", children: title }), count !== undefined && _jsx("span", { className: cx("text-xs font-medium", ink), children: count }), status] }));
17
+ return (_jsxs("div", { className: cx("flex gap-2", status ? "items-center" : "items-baseline", spaced && HEADING_GAP[size]), children: [titleStyle === "name" ? (_jsx(NameTitle, { id: id, children: title })) : (_jsx(Overline, { as: "h2", id: id, color: bar ? "tertiary" : "muted", children: title })), count !== undefined && _jsx("span", { className: cx("text-xs font-medium", ink), children: count }), status] }));
14
18
  }
15
19
  /** A card's `icon`: the feature-size mark above the heading, in the accent colour. */
16
20
  export function CardIcon({ name, size }) {
@@ -1,4 +1,4 @@
1
- import { type ComponentPropsWithoutRef, type ReactElement, type ReactNode } from "react";
1
+ import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from "react";
2
2
  import type { Unstyled } from "./props.js";
3
3
  import type { Size } from "./size.js";
4
4
  /**
package/dist/Checkbox.js CHANGED
@@ -1,24 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useId } from "react";
3
2
  import { cx, focusRing } from "./cx.js";
3
+ import { useDescription, Words } from "./labelWords.js";
4
4
  /** The label's gap and text, the box, and the box's drop onto the first line, by `size`. */
5
5
  const SIZE = {
6
6
  sm: { label: "gap-1.5 text-xs", box: "h-3.5 w-3.5", start: "mt-px" },
7
7
  md: { label: "gap-2 text-sm", box: "h-4 w-4", start: "mt-0.5" },
8
8
  lg: { label: "gap-2.5 text-base", box: "h-5 w-5", start: "mt-0.5" },
9
9
  };
10
- /** The label, with its muted description under it when there is one. */
11
- function Words({ label, description, id, }) {
12
- if (id === undefined)
13
- return label;
14
- return (_jsxs("span", { className: "flex flex-col gap-0.5", children: [_jsx("span", { children: label }), _jsx("span", { id: id, className: "text-xs text-slate-500", children: description })] }));
15
- }
16
- /** The description's id (none without a description) and the box's whole `aria-describedby`. */
17
- function useDescription(description, describedBy) {
18
- const generatedId = useId();
19
- const descriptionId = description != null && description !== false ? generatedId : undefined;
20
- return { descriptionId, describedIds: [describedBy, descriptionId].filter(Boolean).join(" ") };
21
- }
22
10
  /**
23
11
  * A native checkbox with its label. Every prop but the label goes on the input.
24
12
  * `size="sm"` is `text-xs` and a smaller box, for a dense row; `size="lg"` is `text-base` and a
@@ -9,8 +9,10 @@ import type { Size } from "./size.js";
9
9
  * the toggle swaps those classes, and `schemeClass` gives the same one for the server to render on `<html>`.
10
10
  * An app that knows the scheme on the server (from a cookie) passes it as `initial`, so the server
11
11
  * markup, the hydrating client and the `<html>` class agree; persist a change from `onChange`.
12
- * The icon follows the class on `<html>` through CSS, so even without `initial` a dark page shows
13
- * the sun from the first frame; the button's name catches up once the client has hydrated.
12
+ * The icon and the button's accessible name follow the class on `<html>` through CSS (the name is
13
+ * two visually hidden "Switch to …" spans, one hidden by `display:none`), so even without `initial`
14
+ * a dark page shows the sun and is named "Switch to light theme" from the first frame, with no
15
+ * script. Only the tooltip catches up once the client has hydrated.
14
16
  * `store={false}` skips `localStorage`, for an app that keeps the choice in a server cookie and
15
17
  * writes it from `onChange`. Every other button prop (`id`, `data-*`, `aria-*`) goes on the button.
16
18
  */