@hackcove/ui-react 0.10.0 → 0.10.1

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
@@ -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; `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).
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`, `search`, `chevron-right`, `chevron-down`, `close`, `external-link`, `settings`, `bell`, `check`, `plus`: feature ideas and UI glyphs on one 24 grid, a 1.5 stroke, round caps and joins, each the idea or action, 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
@@ -0,0 +1,35 @@
1
+ import { type ReactElement } from "react";
2
+ import type { Passthrough } from "./props.js";
3
+ /**
4
+ * The box a message is typed into, with its button beside it. It keeps its own text. Enter sends
5
+ * the trimmed text and clears the box; Shift+Enter is a newline; empty or whitespace-only text does
6
+ * not send. While `busy` a reply is on its way: the button is Stop, Enter does not send, and typing
7
+ * carries on with the text kept for the next send.
8
+ */
9
+ export declare function ChatComposer({ label, placeholder, disabled, busy, onSend, onStop, sendLabel, stopLabel, ...rest }: {
10
+ /** The box's name for assistive tech; it is not drawn. */
11
+ label: string;
12
+ /** The hint inside the empty box. */
13
+ placeholder?: string | undefined;
14
+ /** Disables the box and the button. For a chat that cannot take a message at all, not for a reply in flight: that is `busy`. */
15
+ disabled?: boolean | undefined;
16
+ /**
17
+ * A reply is on its way: the button is Stop and Enter does not send. With no `onStop`, the button
18
+ * stays a disabled Send and no Stop is drawn.
19
+ */
20
+ busy?: boolean | undefined;
21
+ /** Called with the trimmed text. */
22
+ onSend: (text: string) => void;
23
+ /** Called when the reader presses Stop. */
24
+ onStop?: (() => void) | undefined;
25
+ /**
26
+ * The button's word, for sending.
27
+ * @default Send
28
+ */
29
+ sendLabel?: string | undefined;
30
+ /**
31
+ * The button's word, while `busy`.
32
+ * @default Stop
33
+ */
34
+ stopLabel?: string | undefined;
35
+ } & Passthrough): ReactElement;
@@ -0,0 +1,50 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId, useRef, useState, } from "react";
3
+ import { Button } from "./Button.js";
4
+ import { Cluster, ClusterGrow } from "./Cluster.js";
5
+ import { Field } from "./Field.js";
6
+ import { Textarea } from "./Textarea.js";
7
+ /**
8
+ * An IME is still composing. Safari ends the composition before the Enter's keydown, so there
9
+ * `isComposing` is already false and only the 229 keyCode tells the commit from a real Enter.
10
+ */
11
+ function isComposing({ nativeEvent }) {
12
+ return nativeEvent.isComposing || nativeEvent.keyCode === 229;
13
+ }
14
+ /**
15
+ * The box a message is typed into, with its button beside it. It keeps its own text. Enter sends
16
+ * the trimmed text and clears the box; Shift+Enter is a newline; empty or whitespace-only text does
17
+ * not send. While `busy` a reply is on its way: the button is Stop, Enter does not send, and typing
18
+ * carries on with the text kept for the next send.
19
+ */
20
+ export function ChatComposer({ label, placeholder, disabled = false, busy = false, onSend, onStop, sendLabel = "Send", stopLabel = "Stop", ...rest }) {
21
+ const id = useId();
22
+ const box = useRef(null);
23
+ const [text, setText] = useState("");
24
+ const trimmed = text.trim();
25
+ const stop = busy && onStop !== undefined;
26
+ const send = () => {
27
+ if (busy || trimmed === "")
28
+ return;
29
+ onSend(trimmed);
30
+ setText("");
31
+ };
32
+ const onKeyDown = (event) => {
33
+ if (event.key !== "Enter" || event.shiftKey || isComposing(event))
34
+ return;
35
+ event.preventDefault();
36
+ send();
37
+ };
38
+ // One submit button that is Send or Stop, so it is the same element across the swap and keeps its
39
+ // focus. Send goes back to the box: the empty box has just disabled the button under the focus.
40
+ const onSubmit = (event) => {
41
+ event.preventDefault();
42
+ if (busy) {
43
+ onStop?.();
44
+ return;
45
+ }
46
+ send();
47
+ box.current?.focus();
48
+ };
49
+ return (_jsx("form", { ...rest, onSubmit: onSubmit, children: _jsxs(Cluster, { align: "end", gap: "sm", children: [_jsx(ClusterGrow, { children: _jsx(Field, { label: label, htmlFor: id, hideLabel: true, children: _jsx(Textarea, { ref: box, id: id, rows: 2, value: text, placeholder: placeholder, disabled: disabled, onChange: (event) => setText(event.target.value), onKeyDown: onKeyDown }) }) }), _jsx(Button, { type: "submit", variant: stop ? "outline" : "primary", disabled: disabled || (busy ? !stop : trimmed === ""), children: stop ? stopLabel : sendLabel })] }) }));
50
+ }
@@ -0,0 +1,26 @@
1
+ import type { ReactElement, ReactNode } from "react";
2
+ import type { Passthrough } from "./props.js";
3
+ /**
4
+ * One message in a chat: a bubble on the sender's side, with an optional line above it and a row of
5
+ * actions under it. `children` is the body as the caller wants it (plain text, which keeps its line
6
+ * breaks, a `Markdown`, an `Image`). `streaming` marks a reply still arriving; with no children it
7
+ * draws the caret alone, the pending state.
8
+ */
9
+ export declare function ChatMessage({ role, author, meta, actions, streaming, children, ...rest }: {
10
+ /** Whose side it sits on: `user` on the right on the inset fill, `assistant` on the left on the surface. */
11
+ role: "user" | "assistant";
12
+ /** Who sent it, read first by a screen reader and drawn nowhere: alignment alone tells a screen reader nothing. Show a sender with `meta`. */
13
+ author: string;
14
+ /** A small line above the bubble: a sender and a time, already formatted. */
15
+ meta?: ReactNode;
16
+ /** A row under the bubble on the message's own side: copy, retry, a row of reaction `Pill`s. */
17
+ actions?: ReactNode;
18
+ /**
19
+ * The reply is still arriving: sets `aria-busy` and draws a caret after the body, which pulses
20
+ * unless the reader asks for reduced motion.
21
+ * @default false
22
+ */
23
+ streaming?: boolean | undefined;
24
+ /** The body. It may be empty while `streaming`. */
25
+ children?: ReactNode;
26
+ } & Passthrough): ReactElement;
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from "./cx.js";
3
+ const BUBBLE = "max-w-[90%] rounded-xl px-3 py-2 text-sm text-slate-900";
4
+ const ROLE = {
5
+ user: { side: "items-end", row: "justify-end", bubble: "bg-slate-100" },
6
+ assistant: {
7
+ side: "items-start",
8
+ row: "justify-start",
9
+ bubble: "border border-slate-200 bg-white",
10
+ },
11
+ };
12
+ /**
13
+ * One message in a chat: a bubble on the sender's side, with an optional line above it and a row of
14
+ * actions under it. `children` is the body as the caller wants it (plain text, which keeps its line
15
+ * breaks, a `Markdown`, an `Image`). `streaming` marks a reply still arriving; with no children it
16
+ * draws the caret alone, the pending state.
17
+ */
18
+ export function ChatMessage({ role, author, meta, actions, streaming = false, children, ...rest }) {
19
+ const look = ROLE[role];
20
+ return (_jsxs("article", { ...rest, "aria-busy": streaming || undefined, className: cx("flex w-full min-w-0 flex-col gap-1", look.side), children: [_jsx("span", { className: "sr-only", children: author }), meta && _jsx("div", { className: "text-xs text-slate-500", children: meta }), _jsxs("div", { className: cx(BUBBLE, look.bubble, "wrap-anywhere whitespace-pre-wrap"), children: [children, streaming && (_jsx("span", { "aria-hidden": "true", className: "text-slate-400 motion-safe:animate-pulse", children: "\u258D" }))] }), actions && (_jsx("div", { className: cx("flex flex-wrap items-center gap-1", look.row), children: actions }))] }));
21
+ }
@@ -0,0 +1,25 @@
1
+ import { type ReactElement, type ReactNode } from "react";
2
+ import type { Passthrough } from "./props.js";
3
+ /** Whether a box scrolled this far is at its bottom, give or take `BOTTOM_SLACK`. */
4
+ export declare function isAtBottom(scrollTop: number, clientHeight: number, scrollHeight: number): boolean;
5
+ /**
6
+ * The scrolling box of a chat: its children are the messages, tool cards and anything else, a gap
7
+ * apart. It follows the newest content unless the reader has scrolled up. It is a named region and
8
+ * a tab stop, so a keyboard can scroll it. It is deliberately not a live region, which would read
9
+ * every streamed token aloud: set `announce` when a message is finished.
10
+ */
11
+ export declare function ChatThread({ label, height, empty, announce, children, ...rest }: {
12
+ /** Names the region. */
13
+ label: string;
14
+ /**
15
+ * The tallest it gets before it scrolls: `sm` 20rem, `md` 28rem, `lg` 40rem. `fill` takes the
16
+ * height of a parent that sets one.
17
+ * @default md
18
+ */
19
+ height?: "sm" | "md" | "lg" | "fill" | undefined;
20
+ /** Drawn instead when there are no children: an `EmptyState`. */
21
+ empty?: ReactNode;
22
+ /** Text a screen reader reads out politely when it changes. Set it when a message is finished, not on each token. */
23
+ announce?: string | undefined;
24
+ children?: ReactNode;
25
+ } & Passthrough): ReactElement;
@@ -0,0 +1,59 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Children, useLayoutEffect, useRef } from "react";
3
+ import { cx, focusRing, themedScrollbar } from "./cx.js";
4
+ const HEIGHT = {
5
+ sm: "max-h-80",
6
+ md: "max-h-[28rem]",
7
+ lg: "max-h-[40rem]",
8
+ fill: "h-full min-h-0",
9
+ };
10
+ /** How far above the very bottom, in pixels, still counts as reading the latest message. */
11
+ const BOTTOM_SLACK = 32;
12
+ /** Whether a box scrolled this far is at its bottom, give or take `BOTTOM_SLACK`. */
13
+ export function isAtBottom(scrollTop, clientHeight, scrollHeight) {
14
+ return scrollHeight - scrollTop - clientHeight <= BOTTOM_SLACK;
15
+ }
16
+ /**
17
+ * Starts the box at the bottom and keeps it there as the content grows, until the reader scrolls
18
+ * up; scrolling back to the bottom re-arms it. The box is watched as well as the content: a box
19
+ * that gets shorter (a phone's keyboard opening under `fill`) has moved the bottom out of view too.
20
+ */
21
+ function useStickToBottom() {
22
+ const box = useRef(null);
23
+ const content = useRef(null);
24
+ const following = useRef(true);
25
+ useLayoutEffect(() => {
26
+ const scroller = box.current;
27
+ if (!scroller || !content.current)
28
+ return undefined;
29
+ const follow = () => {
30
+ if (following.current)
31
+ scroller.scrollTop = scroller.scrollHeight;
32
+ };
33
+ follow();
34
+ // jsdom (a consumer's own tests) has no ResizeObserver.
35
+ if (typeof ResizeObserver === "undefined")
36
+ return undefined;
37
+ const observer = new ResizeObserver(follow);
38
+ observer.observe(content.current);
39
+ observer.observe(scroller);
40
+ return () => observer.disconnect();
41
+ }, []);
42
+ const onScroll = () => {
43
+ const scroller = box.current;
44
+ if (!scroller)
45
+ return;
46
+ following.current = isAtBottom(scroller.scrollTop, scroller.clientHeight, scroller.scrollHeight);
47
+ };
48
+ return { box, content, onScroll };
49
+ }
50
+ /**
51
+ * The scrolling box of a chat: its children are the messages, tool cards and anything else, a gap
52
+ * apart. It follows the newest content unless the reader has scrolled up. It is a named region and
53
+ * a tab stop, so a keyboard can scroll it. It is deliberately not a live region, which would read
54
+ * every streamed token aloud: set `announce` when a message is finished.
55
+ */
56
+ export function ChatThread({ label, height = "md", empty, announce, children, ...rest }) {
57
+ const { box, content, onScroll } = useStickToBottom();
58
+ return (_jsxs(_Fragment, { children: [_jsx("div", { "aria-live": "polite", className: "sr-only", children: announce }), _jsx("div", { ...rest, ref: box, role: "region", "aria-label": label, tabIndex: 0, onScroll: onScroll, className: cx("overflow-y-auto rounded-lg", HEIGHT[height], themedScrollbar, focusRing), children: _jsx("div", { ref: content, className: "flex flex-col gap-3 text-slate-900", children: Children.toArray(children).length === 0 ? empty : children }) })] }));
59
+ }
package/dist/Icon.d.ts CHANGED
@@ -14,8 +14,10 @@ type IconProps = Passthrough & {
14
14
  /**
15
15
  * One inline icon, sized to a fixed box (sm 14px, md 16px, lg 20px, `feature` 32px) and drawn in
16
16
  * `currentColor`, so it takes the text colour around it unless a `tone` says otherwise. `name`
17
- * picks one of the library's own icons, all drawn on one grid with one stroke (`diff`,
18
- * `baseline`, `review`, `ci`, `self-host`, `privacy`, `capture`, `branch`, `theme`, `speed`);
17
+ * picks one of the library's own icons, all drawn on one grid with one stroke: feature ideas
18
+ * (`diff`, `baseline`, `review`, `ci`, `self-host`, `privacy`, `capture`, `branch`, `theme`,
19
+ * `speed`, `search`) and UI glyphs (`chevron-right`, `chevron-down`, `close`, `external-link`,
20
+ * `settings`, `bell`, `check`, `plus`);
19
21
  * without a name, a bare `<svg>` child fills the box. It is decorative (`aria-hidden`) by
20
22
  * default; a `label` makes it an image with that name, for an icon that carries meaning no text
21
23
  * beside it says.
package/dist/Icon.js CHANGED
@@ -11,8 +11,10 @@ const BOX = {
11
11
  /**
12
12
  * One inline icon, sized to a fixed box (sm 14px, md 16px, lg 20px, `feature` 32px) and drawn in
13
13
  * `currentColor`, so it takes the text colour around it unless a `tone` says otherwise. `name`
14
- * picks one of the library's own icons, all drawn on one grid with one stroke (`diff`,
15
- * `baseline`, `review`, `ci`, `self-host`, `privacy`, `capture`, `branch`, `theme`, `speed`);
14
+ * picks one of the library's own icons, all drawn on one grid with one stroke: feature ideas
15
+ * (`diff`, `baseline`, `review`, `ci`, `self-host`, `privacy`, `capture`, `branch`, `theme`,
16
+ * `speed`, `search`) and UI glyphs (`chevron-right`, `chevron-down`, `close`, `external-link`,
17
+ * `settings`, `bell`, `check`, `plus`);
16
18
  * without a name, a bare `<svg>` child fills the box. It is decorative (`aria-hidden`) by
17
19
  * default; a `label` makes it an image with that name, for an icon that carries meaning no text
18
20
  * beside it says.
package/dist/Input.js CHANGED
@@ -13,7 +13,7 @@ export function Input({ icon, prefix, size = "md", width = "full", align = "star
13
13
  return _jsx(SearchInput, { ...rest, icon: icon, size: size, width: width, align: align });
14
14
  }
15
15
  if (prefix !== undefined) {
16
- return (_jsx(PrefixShell, { prefix: prefix, size: size, width: width, disabled: rest.disabled, children: _jsx("input", { ...rest, className: cx("min-w-0 flex-1 bg-transparent px-1 outline-none placeholder:text-slate-400", CONTROL_ALIGN[align]) }) }));
16
+ return (_jsx(PrefixShell, { prefix: prefix, size: size, width: width, disabled: rest.disabled, children: _jsx("input", { ...rest, className: cx("min-w-0 flex-1 bg-transparent px-1 text-ellipsis outline-none placeholder:text-slate-400", CONTROL_ALIGN[align]) }) }));
17
17
  }
18
18
  if (icon === undefined) {
19
19
  return (_jsx("input", { ...rest, className: cx(controlClass(size, width), CONTROL_PAD[size].x, CONTROL_ALIGN[align]) }));
@@ -1,6 +1,14 @@
1
1
  import type { ReactElement, ReactNode } from "react";
2
2
  import { type ControlProps } from "./control.js";
3
- /** The relative wrapper of an `Input` that holds more than the field: a leading `icon` and, for search, the clear button. Not exported from the package. */
3
+ /**
4
+ * The relative wrapper of an `Input` that holds more than the field: a leading `icon` and, for search, the clear button. Not exported from the package.
5
+ *
6
+ * Inside a Card or row (`liftInteractive`, which is `isolate`) the field is lifted to `z-10`, and
7
+ * its opaque background would paint over a z-auto icon. So the icon ranks `z-20` there. It is
8
+ * scoped to an `isolate` ancestor so a field elsewhere on the page never outranks AppShell's
9
+ * sticky header (`z-10`). The clear button is a lifted `button` and comes after the field, so it
10
+ * already sits above it.
11
+ */
4
12
  export declare function InputShell({ icon, size, width, children, }: {
5
13
  icon?: ReactNode;
6
14
  children: ReactNode;
@@ -1,11 +1,15 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { CONTROL_WIDTH, controlClass } from "./control.js";
3
3
  import { cx } from "./cx.js";
4
- /** Where a leading icon sits, and its size, by the control's size. */
4
+ /**
5
+ * Where a leading icon sits, and its size, by the control's size. The wrapper is a flex box centred
6
+ * on the field, so the icon's own box centres too, and its one child is sized whether it is a bare
7
+ * `<svg>` or an `Icon`.
8
+ */
5
9
  const ICON = {
6
- sm: "left-2 [&>svg]:h-3.5 [&>svg]:w-3.5",
7
- md: "left-3 [&>svg]:h-4 [&>svg]:w-4",
8
- lg: "left-4 [&>svg]:h-5 [&>svg]:w-5",
10
+ sm: "left-2 [&>*]:size-3.5",
11
+ md: "left-3 [&>*]:size-4",
12
+ lg: "left-4 [&>*]:size-5",
9
13
  };
10
14
  /** The prefix text's inset and size, by the control's size. */
11
15
  const PREFIX = {
@@ -13,9 +17,17 @@ const PREFIX = {
13
17
  md: "pl-3 text-sm",
14
18
  lg: "pl-4 text-base",
15
19
  };
16
- /** The relative wrapper of an `Input` that holds more than the field: a leading `icon` and, for search, the clear button. Not exported from the package. */
20
+ /**
21
+ * The relative wrapper of an `Input` that holds more than the field: a leading `icon` and, for search, the clear button. Not exported from the package.
22
+ *
23
+ * Inside a Card or row (`liftInteractive`, which is `isolate`) the field is lifted to `z-10`, and
24
+ * its opaque background would paint over a z-auto icon. So the icon ranks `z-20` there. It is
25
+ * scoped to an `isolate` ancestor so a field elsewhere on the page never outranks AppShell's
26
+ * sticky header (`z-10`). The clear button is a lifted `button` and comes after the field, so it
27
+ * already sits above it.
28
+ */
17
29
  export function InputShell({ icon, size = "md", width = "full", children, }) {
18
- return (_jsxs("div", { className: cx("relative", width === "auto" && "inline-block", width !== "full" && CONTROL_WIDTH[width]), children: [icon !== undefined && (_jsx("span", { "aria-hidden": "true", className: cx("pointer-events-none absolute top-1/2 -translate-y-1/2 text-slate-400", ICON[size]), children: icon })), children] }));
30
+ return (_jsxs("div", { className: cx("relative", width === "auto" && "inline-block", width !== "full" && CONTROL_WIDTH[width]), children: [icon !== undefined && (_jsx("span", { "aria-hidden": "true", className: cx("pointer-events-none absolute top-1/2 flex -translate-y-1/2 items-center leading-none text-slate-400 in-[.isolate]:z-20", ICON[size]), children: icon })), children] }));
19
31
  }
20
32
  /**
21
33
  * The field box of an `Input` with a `prefix`: the control look and the focus ring sit on the
@@ -0,0 +1,16 @@
1
+ import type { ReactElement, ReactNode } from "react";
2
+ import type { Passthrough } from "./props.js";
3
+ /**
4
+ * One tool call in a chat: a closed panel whose line says which tool, what it was doing and how it
5
+ * went; opening it shows the arguments and the result. The status is a word, not only a colour.
6
+ */
7
+ export declare function ToolCallCard({ name, status, summary, children, ...rest }: {
8
+ /** The tool's name, set in the monospace face. */
9
+ name: string;
10
+ /** How the call went: `running`, `done` or `error`, drawn as Running, Done or Failed. */
11
+ status: "running" | "done" | "error";
12
+ /** One line on what it did; a long one is cut off with an ellipsis. */
13
+ summary?: string | undefined;
14
+ /** The arguments and the result, shown while open. */
15
+ children: ReactNode;
16
+ } & Passthrough): ReactElement;
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Disclosure } from "./Disclosure.js";
3
+ import { Pill } from "./Pill.js";
4
+ const STATUS = {
5
+ running: { word: "Running", tone: "slate" },
6
+ done: { word: "Done", tone: "emerald" },
7
+ error: { word: "Failed", tone: "red" },
8
+ };
9
+ /**
10
+ * One tool call in a chat: a closed panel whose line says which tool, what it was doing and how it
11
+ * went; opening it shows the arguments and the result. The status is a word, not only a colour.
12
+ */
13
+ export function ToolCallCard({ name, status, summary, children, ...rest }) {
14
+ const { word, tone } = STATUS[status];
15
+ return (_jsx(Disclosure, { ...rest, bordered: true, summary: _jsxs("span", { className: "flex min-w-0 flex-1 items-center gap-2", children: [_jsx("span", { className: "max-w-1/2 shrink-0 truncate font-mono", children: name }), _jsx("span", { className: "min-w-0 flex-1 truncate font-normal text-slate-500", children: summary }), _jsx("span", { className: "shrink-0", children: _jsx(Pill, { tone: tone, children: word }) })] }), children: children }));
16
+ }
package/dist/control.js CHANGED
@@ -31,7 +31,7 @@ export function controlClass(size = "md", width = "full") {
31
31
  return [
32
32
  CONTROL_WIDTH[width],
33
33
  CONTROL_SIZE[size],
34
- `rounded-lg border border-slate-300 bg-white text-slate-900 placeholder:text-slate-400 transition hover:border-violet-400 disabled:opacity-50 ${focusRing}`,
34
+ `rounded-lg border border-slate-300 bg-white text-slate-900 placeholder:text-slate-400 text-ellipsis transition hover:border-violet-400 disabled:opacity-50 ${focusRing}`,
35
35
  ]
36
36
  .filter(Boolean)
37
37
  .join(" ");
package/dist/icons.d.ts CHANGED
@@ -4,10 +4,10 @@ import type { ReactElement } from "react";
4
4
  * two units of padding (its ink lives in 2..22), is drawn as an outline with a 1.5 stroke, round
5
5
  * caps and round joins, and never fills: `GLYPH` below is the only place those are set, so an
6
6
  * icon's own shapes carry no `fill`, `stroke` or `stroke-width` (`Icon.test.tsx` holds them to
7
- * that). A name is the idea the icon stands for, not a product.
7
+ * that). A name is the idea or action the icon stands for, not a product.
8
8
  */
9
- declare const NAMES: readonly ["diff", "baseline", "review", "ci", "self-host", "privacy", "capture", "branch", "theme", "speed"];
10
- /** The names of the icon set, one per idea. */
9
+ declare const NAMES: readonly ["diff", "baseline", "review", "ci", "self-host", "privacy", "capture", "branch", "theme", "speed", "search", "chevron-right", "chevron-down", "close", "external-link", "settings", "bell", "check", "plus"];
10
+ /** The names of the icon set: feature ideas, then UI glyphs. */
11
11
  export type IconName = (typeof NAMES)[number];
12
12
  /** The icon `name` as an `<svg>`: the set's one grid, stroke, caps and joins, drawn in `currentColor`. */
13
13
  export declare function iconGlyph(name: IconName): ReactElement;
package/dist/icons.js CHANGED
@@ -4,7 +4,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
4
4
  * two units of padding (its ink lives in 2..22), is drawn as an outline with a 1.5 stroke, round
5
5
  * caps and round joins, and never fills: `GLYPH` below is the only place those are set, so an
6
6
  * icon's own shapes carry no `fill`, `stroke` or `stroke-width` (`Icon.test.tsx` holds them to
7
- * that). A name is the idea the icon stands for, not a product.
7
+ * that). A name is the idea or action the icon stands for, not a product.
8
8
  */
9
9
  const NAMES = [
10
10
  "diff",
@@ -17,6 +17,15 @@ const NAMES = [
17
17
  "branch",
18
18
  "theme",
19
19
  "speed",
20
+ "search",
21
+ "chevron-right",
22
+ "chevron-down",
23
+ "close",
24
+ "external-link",
25
+ "settings",
26
+ "bell",
27
+ "check",
28
+ "plus",
20
29
  ];
21
30
  const GLYPHS = {
22
31
  /** Before and after, side by side: a minus on the left, a plus on the right. */
@@ -39,6 +48,24 @@ const GLYPHS = {
39
48
  theme: (_jsx("path", { d: "M12 3.5S5.75 9.9 5.75 14.25a6.25 6.25 0 0 0 12.5 0C18.25 9.9 12 3.5 12 3.5ZM9 14.5a3 3 0 0 0 3 3" })),
40
49
  /** Fast: a bolt. */
41
50
  speed: _jsx("path", { d: "M13.25 3 5 13.5h6.25L10.75 21 19 10.5h-6.25Z" }),
51
+ /** Finding something: a magnifying glass, a circle with its handle to the lower right. */
52
+ search: (_jsxs(_Fragment, { children: [_jsx("circle", { cx: "10.5", cy: "10.5", r: "6.5" }), _jsx("path", { d: "M15.5 15.5 20.5 20.5" })] })),
53
+ /** A link or next step: an open chevron pointing right. */
54
+ "chevron-right": _jsx("path", { d: "m9 5.5 6.5 6.5L9 18.5" }),
55
+ /** A disclosure or menu: an open chevron pointing down. */
56
+ "chevron-down": _jsx("path", { d: "m5.5 9 6.5 6.5L18.5 9" }),
57
+ /** Dismiss: two strokes crossing in an X. */
58
+ close: _jsx("path", { d: "m5.5 5.5 13 13M18.5 5.5l-13 13" }),
59
+ /** Leaves the site: a box open at its top right, with an arrow out to the top right. */
60
+ "external-link": (_jsxs(_Fragment, { children: [_jsx("path", { d: "M10.5 6H7a3 3 0 0 0-3 3v8a3 3 0 0 0 3 3h8a3 3 0 0 0 3-3v-3.5" }), _jsx("path", { d: "M14 4h6v6M20 4l-9.5 9.5" })] })),
61
+ /** Preferences: a cog, a ring of teeth round a hole. */
62
+ settings: (_jsxs(_Fragment, { children: [_jsx("path", { d: "M10.59 5.35L10.75 3.09L13.25 3.09L13.41 5.35A6.8 6.8 0 0 1 15.7 6.3L17.42 4.81L19.19 6.58L17.7 8.3A6.8 6.8 0 0 1 18.65 10.59L20.91 10.75L20.91 13.25L18.65 13.41A6.8 6.8 0 0 1 17.7 15.7L19.19 17.42L17.42 19.19L15.7 17.7A6.8 6.8 0 0 1 13.41 18.65L13.25 20.91L10.75 20.91L10.59 18.65A6.8 6.8 0 0 1 8.3 17.7L6.58 19.19L4.81 17.42L6.3 15.7A6.8 6.8 0 0 1 5.35 13.41L3.09 13.25L3.09 10.75L5.35 10.59A6.8 6.8 0 0 1 6.3 8.3L4.81 6.58L6.58 4.81L8.3 6.3A6.8 6.8 0 0 1 10.59 5.35Z" }), _jsx("circle", { cx: "12", cy: "12", r: "2.75" })] })),
63
+ /** Notifications: a bell with its clapper. */
64
+ bell: (_jsxs(_Fragment, { children: [_jsx("path", { d: "M6 9.5a6 6 0 0 1 12 0c0 5 2 6.5 2 6.5H4s2-1.5 2-6.5Z" }), _jsx("path", { d: "M10 19.5a2.2 2.2 0 0 0 4 0" })] })),
65
+ /** Done or selected: a single tick. */
66
+ check: _jsx("path", { d: "m4.5 12.5 5 5L19.5 7" }),
67
+ /** Add or create: two strokes crossing, upright. */
68
+ plus: _jsx("path", { d: "M12 5v14M5 12h14" }),
42
69
  };
43
70
  /** The icon `name` as an `<svg>`: the set's one grid, stroke, caps and joins, drawn in `currentColor`. */
44
71
  export function iconGlyph(name) {
package/dist/index.d.ts CHANGED
@@ -33,6 +33,9 @@ export { TableEditCell } from "./TableEditCell.js";
33
33
  export { EmptyState } from "./EmptyState.js";
34
34
  export { Step, Steps } from "./Steps.js";
35
35
  export { Timeline, TimelineItem } from "./Timeline.js";
36
+ export { ChatComposer } from "./ChatComposer.js";
37
+ export { ChatMessage } from "./ChatMessage.js";
38
+ export { ChatThread } from "./ChatThread.js";
36
39
  export { Code } from "./Code.js";
37
40
  export { CodeBlock } from "./CodeBlock.js";
38
41
  export type { CodeLanguage } from "./highlight.js";
@@ -73,6 +76,7 @@ export { Image, type ImageAspect, type ImageProps, type ImageWidth } from "./Ima
73
76
  export { MediaTile, type MediaTileProps } from "./MediaTile.js";
74
77
  export { ScrollStrip, type ScrollStripProps } from "./ScrollStrip.js";
75
78
  export { TextLink, type TextLinkSize } from "./TextLink.js";
79
+ export { ToolCallCard } from "./ToolCallCard.js";
76
80
  export { Caption, Display, Lead, Overline, PageHeading, SectionHeading, SubsectionHeading, type SubsectionHeadingSize, Text, type TextTone, type TypographyColor, type TypographyProps, } from "./typography.js";
77
81
  export { DateField, type DateFieldProps } from "./DateField.js";
78
82
  export { fromISODate, toISODate } from "./isoDate.js";
package/dist/index.js CHANGED
@@ -33,6 +33,9 @@ export { TableEditCell } from "./TableEditCell.js";
33
33
  export { EmptyState } from "./EmptyState.js";
34
34
  export { Step, Steps } from "./Steps.js";
35
35
  export { Timeline, TimelineItem } from "./Timeline.js";
36
+ export { ChatComposer } from "./ChatComposer.js";
37
+ export { ChatMessage } from "./ChatMessage.js";
38
+ export { ChatThread } from "./ChatThread.js";
36
39
  export { Code } from "./Code.js";
37
40
  export { CodeBlock } from "./CodeBlock.js";
38
41
  export { CodeExample } from "./CodeExample.js";
@@ -69,6 +72,7 @@ export { Image } from "./Image.js";
69
72
  export { MediaTile } from "./MediaTile.js";
70
73
  export { ScrollStrip } from "./ScrollStrip.js";
71
74
  export { TextLink } from "./TextLink.js";
75
+ export { ToolCallCard } from "./ToolCallCard.js";
72
76
  export { Caption, Display, Lead, Overline, PageHeading, SectionHeading, SubsectionHeading, Text, } from "./typography.js";
73
77
  export { DateField } from "./DateField.js";
74
78
  export { fromISODate, toISODate } from "./isoDate.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hackcove/ui-react",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "Hack Cove React components: primitives, typography roles and a safe Markdown renderer, written against Tailwind v4 utilities. Pair with a theme from @hackcove/themes.",
5
5
  "keywords": [
6
6
  "components",