@hackcove/ui-react 0.9.2 → 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.
Files changed (74) hide show
  1. package/MIGRATING.md +21 -0
  2. package/README.md +24 -9
  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/ChatComposer.d.ts +35 -0
  13. package/dist/ChatComposer.js +50 -0
  14. package/dist/ChatMessage.d.ts +26 -0
  15. package/dist/ChatMessage.js +21 -0
  16. package/dist/ChatThread.d.ts +25 -0
  17. package/dist/ChatThread.js +59 -0
  18. package/dist/Checkbox.d.ts +1 -1
  19. package/dist/Checkbox.js +1 -13
  20. package/dist/ColourSchemeToggle.d.ts +4 -2
  21. package/dist/ColourSchemeToggle.js +23 -10
  22. package/dist/Dialog.d.ts +2 -1
  23. package/dist/Dialog.js +44 -3
  24. package/dist/Disclosure.d.ts +10 -1
  25. package/dist/Disclosure.js +13 -3
  26. package/dist/Field.d.ts +9 -1
  27. package/dist/Field.js +5 -3
  28. package/dist/Icon.d.ts +4 -2
  29. package/dist/Icon.js +4 -2
  30. package/dist/IconButton.d.ts +2 -0
  31. package/dist/IconButton.js +2 -2
  32. package/dist/Image.d.ts +14 -4
  33. package/dist/Image.js +22 -6
  34. package/dist/InlineEdit.d.ts +22 -0
  35. package/dist/InlineEdit.js +60 -0
  36. package/dist/Input.js +1 -1
  37. package/dist/InputShell.d.ts +9 -1
  38. package/dist/InputShell.js +18 -6
  39. package/dist/KeyValueList.d.ts +9 -2
  40. package/dist/KeyValueList.js +8 -3
  41. package/dist/Lightbox.d.ts +2 -0
  42. package/dist/Lightbox.js +2 -1
  43. package/dist/ListRow.d.ts +11 -0
  44. package/dist/ListRow.js +28 -6
  45. package/dist/MediaTile.d.ts +11 -3
  46. package/dist/MediaTile.js +10 -7
  47. package/dist/PageLayouts.d.ts +9 -3
  48. package/dist/PageLayouts.js +9 -6
  49. package/dist/SideColumn.d.ts +26 -0
  50. package/dist/SideColumn.js +22 -0
  51. package/dist/SidebarLayout.d.ts +5 -2
  52. package/dist/SidebarLayout.js +7 -3
  53. package/dist/Stack.d.ts +10 -2
  54. package/dist/Stack.js +5 -3
  55. package/dist/StatusPages.d.ts +13 -3
  56. package/dist/StatusPages.js +8 -5
  57. package/dist/Switch.d.ts +42 -0
  58. package/dist/Switch.js +56 -0
  59. package/dist/TableEditCell.d.ts +9 -7
  60. package/dist/TableEditCell.js +12 -53
  61. package/dist/ToolCallCard.d.ts +16 -0
  62. package/dist/ToolCallCard.js +16 -0
  63. package/dist/control.js +1 -1
  64. package/dist/foldedColumn.d.ts +18 -0
  65. package/dist/foldedColumn.js +45 -0
  66. package/dist/icons.d.ts +3 -3
  67. package/dist/icons.js +28 -1
  68. package/dist/index.d.ts +7 -0
  69. package/dist/index.js +7 -0
  70. package/dist/labelWords.d.ts +12 -0
  71. package/dist/labelWords.js +14 -0
  72. package/dist/scrollStop.d.ts +27 -0
  73. package/dist/scrollStop.js +68 -0
  74. package/package.json +1 -1
@@ -1,5 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { cx, focusRing } from "./cx.js";
2
+ import { cx } from "./cx.js";
3
+ import { FoldedColumn } from "./foldedColumn.js";
4
+ import { scrollStopProps, useScrollStop } from "./scrollStop.js";
3
5
  import { STICKY_ASIDE_MD, STICKY_SCROLL_MD } from "./stickyColumn.js";
4
6
  /**
5
7
  * A `rail` of links beside the `children` canvas, in the Container the shell already gives every
@@ -10,9 +12,11 @@ import { STICKY_ASIDE_MD, STICKY_SCROLL_MD } from "./stickyColumn.js";
10
12
  * a rail taller than the screen scrolls itself, so its last link is reachable. Below `md` it stacks
11
13
  * above the canvas and nothing sticks. A long rail sets `railLabel`:
12
14
  * below `md` it then folds behind a disclosure of that name, closed, so the page comes first on a
13
- * phone, and from `md` it is the same rail. Upgrading from 0.2: the rail no longer has a fill, a
15
+ * phone, and from `md` it is the same rail; `railLabel` also names the rail's `<aside>` landmark. Upgrading from 0.2: the rail no longer has a fill, a
14
16
  * border or a fixed height, and it no longer sits beside the canvas on a phone.
15
17
  */
16
18
  export function SidebarLayout({ rail, railLabel, children, ...rest }) {
17
- return (_jsxs("div", { ...rest, className: "flex flex-col gap-8 md:flex-row md:gap-10", children: [_jsx("aside", { className: cx("w-full shrink-0 md:w-56", STICKY_ASIDE_MD), children: _jsx("div", { className: STICKY_SCROLL_MD, children: railLabel === undefined ? (rail) : (_jsxs("details", { className: "group md:[&::details-content]:[content-visibility:visible]", children: [_jsxs("summary", { className: cx("flex cursor-pointer list-none items-center justify-between rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm font-medium text-slate-900 select-none hover:bg-slate-50 md:hidden [&::-webkit-details-marker]:hidden", focusRing), children: [railLabel, _jsx("svg", { viewBox: "0 0 20 20", width: "16", height: "16", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", className: "shrink-0 text-slate-500 group-open:rotate-180", children: _jsx("path", { d: "M5 8l5 5 5-5" }) })] }), _jsx("div", { className: "mt-2 md:mt-0", children: rail })] })) }) }), _jsx("div", { className: "min-w-0 flex-1", children: children })] }));
19
+ const { attach, stop } = useScrollStop();
20
+ const { props, ring } = scrollStopProps(stop, { label: railLabel });
21
+ return (_jsxs("div", { ...rest, className: "flex flex-col gap-8 md:flex-row md:gap-10", children: [_jsx("aside", { className: cx("w-full shrink-0 md:w-56", STICKY_ASIDE_MD), "aria-label": railLabel, children: _jsx("div", { ref: attach, ...props, className: cx(STICKY_SCROLL_MD, ring), children: railLabel === undefined ? (rail) : (_jsx(FoldedColumn, { from: "md", label: railLabel, children: rail })) }) }), _jsx("div", { className: "min-w-0 flex-1", children: children })] }));
18
22
  }
package/dist/Stack.d.ts CHANGED
@@ -62,13 +62,21 @@ type StackProps = StackBase & (({
62
62
  } & Unstyled<Omit<ComponentPropsWithoutRef<"ol">, "children">>));
63
63
  /**
64
64
  * A `<section>` Stack with an optional `SectionHeading` title (which names it) and `actions` beside
65
- * it. Its blocks are `gap` apart, `md` by default as in a `Stack`.
65
+ * it. Its blocks are `gap` apart, `md` by default as in a `Stack`. `rule` draws a hairline under the
66
+ * title row, `actions` included, as `SectionHeading`'s `rule` does for a bare heading. A line
67
+ * between two sections is a `Divider` in the `Stack` that holds them.
66
68
  */
67
- export declare function Section({ title, actions, gap, width, children, ...rest }: {
69
+ export declare function Section({ title, actions, rule, gap, width, children, ...rest }: {
68
70
  /** A `SectionHeading` at the top, which names the section. */
69
71
  title?: ReactNode;
70
72
  /** Buttons or links at the end of the title row. */
71
73
  actions?: ReactNode;
74
+ /**
75
+ * A hairline under the title row across the section's width, `actions` included. Needs a `title`
76
+ * or `actions`.
77
+ * @default false
78
+ */
79
+ rule?: boolean | undefined;
72
80
  /**
73
81
  * The space between blocks: `xs` gap-2, `sm` gap-4, `md` gap-6, `lg` gap-8, `xl` gap-12 (gap-16 from `sm`).
74
82
  * @default md
package/dist/Stack.js CHANGED
@@ -49,9 +49,11 @@ function Box({ tag: Tag, ...props }) {
49
49
  }
50
50
  /**
51
51
  * A `<section>` Stack with an optional `SectionHeading` title (which names it) and `actions` beside
52
- * it. Its blocks are `gap` apart, `md` by default as in a `Stack`.
52
+ * it. Its blocks are `gap` apart, `md` by default as in a `Stack`. `rule` draws a hairline under the
53
+ * title row, `actions` included, as `SectionHeading`'s `rule` does for a bare heading. A line
54
+ * between two sections is a `Divider` in the `Stack` that holds them.
53
55
  */
54
- export function Section({ title, actions, gap = "md", width = "default", children, ...rest }) {
56
+ export function Section({ title, actions, rule = false, gap = "md", width = "default", children, ...rest }) {
55
57
  const titleId = useId();
56
- return (_jsxs(Stack, { as: "section", ...rest, gap: gap, width: width, "aria-labelledby": title ? titleId : rest["aria-labelledby"], children: [(title || actions) && (_jsxs("div", { className: "flex items-center justify-between gap-4", children: [title && _jsx(SectionHeading, { id: titleId, children: title }), actions && _jsx("div", { className: "ml-auto flex items-center gap-2", children: actions })] })), children] }));
58
+ return (_jsxs(Stack, { as: "section", ...rest, gap: gap, width: width, "aria-labelledby": title ? titleId : rest["aria-labelledby"], children: [(title || actions) && (_jsxs("div", { className: cx("flex items-center justify-between gap-4", rule && "border-b border-slate-200/70 pb-2"), children: [title && _jsx(SectionHeading, { id: titleId, children: title }), actions && _jsx("div", { className: "ml-auto flex items-center gap-2", children: actions })] })), children] }));
57
59
  }
@@ -6,15 +6,25 @@ type StatusPageProps = {
6
6
  title?: ReactNode;
7
7
  /** Replaces the default body copy. */
8
8
  children?: ReactNode;
9
- /** The primary action; the home link turns outline beside it. */
9
+ /**
10
+ * The primary action; the home link turns outline beside it. This is the one way to a button
11
+ * worded differently: pass `action` and leave out `homeHref`, so there is no "Back to home" link.
12
+ * There is no label prop.
13
+ */
10
14
  action?: ReactNode;
11
15
  /** Adds a "Back to home" button linking here. With neither this nor `renderHome` there is no home link. */
12
16
  homeHref?: string;
13
17
  /** Draws the home link with the app's own router `<Link>`, as on `Button`. */
14
18
  renderHome?: LinkRender;
15
19
  } & Passthrough;
16
- /** 404: the page is not there. */
17
- export declare function NotFound(props: StatusPageProps): ReactElement;
20
+ /**
21
+ * 404: the page is not there. `numeral` draws a large "404" above the title, for a site whose 404
22
+ * is a designed page; the digits are fixed. Off, the page is the title and copy alone.
23
+ */
24
+ export declare function NotFound({ numeral, ...props }: StatusPageProps & {
25
+ /** Draws a large "404" above the title. Off by default. The digits are fixed: there is no text prop. */
26
+ numeral?: boolean | undefined;
27
+ }): ReactElement;
18
28
  /**
19
29
  * 500 or any failure. `detail` is the error text, shown in a `CodeBlock` under the copy; pass it
20
30
  * only where a reader may see it (a request id, a message), never a stack trace. `onRetry` adds a
@@ -11,17 +11,20 @@ function homeLink(variant, href, render) {
11
11
  return null;
12
12
  return (_jsx(Button, { href: href, variant: variant, children: "Back to home" }));
13
13
  }
14
- function StatusPage({ copy, title, children, action, homeHref, renderHome, extra, ...rest }) {
14
+ function StatusPage({ copy, title, children, action, homeHref, renderHome, extra, mark, ...rest }) {
15
15
  const home = homeLink(action === undefined ? "primary" : "outline", homeHref, renderHome);
16
- return (_jsxs(CenteredLayout, { ...rest, title: title ?? copy.title, subtitle: children ?? copy.body, children: [extra, (action || home) && (_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [action, home] }))] }));
16
+ return (_jsxs(CenteredLayout, { ...rest, mark: mark, title: title ?? copy.title, subtitle: children ?? copy.body, children: [extra, (action || home) && (_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [action, home] }))] }));
17
17
  }
18
18
  const retry = (onRetry) => onRetry && _jsx(Button, { onClick: onRetry, children: "Try again" });
19
- /** 404: the page is not there. */
20
- export function NotFound(props) {
19
+ /**
20
+ * 404: the page is not there. `numeral` draws a large "404" above the title, for a site whose 404
21
+ * is a designed page; the digits are fixed. Off, the page is the title and copy alone.
22
+ */
23
+ export function NotFound({ numeral = false, ...props }) {
21
24
  return (_jsx(StatusPage, { copy: {
22
25
  title: "Page not found",
23
26
  body: "The page you are looking for does not exist or has moved.",
24
- }, ...props }));
27
+ }, mark: numeral ? (_jsx("p", { className: "text-6xl font-semibold tracking-tight text-violet-600", children: "404" })) : undefined, ...props }));
25
28
  }
26
29
  /**
27
30
  * 500 or any failure. `detail` is the error text, shown in a `CodeBlock` under the copy; pass it
@@ -0,0 +1,42 @@
1
+ import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from "react";
2
+ import type { Unstyled } from "./props.js";
3
+ import type { Size } from "./size.js";
4
+ type Naming = {
5
+ /** The text beside the switch; the whole label is a click target. */
6
+ label: NonNullable<ReactNode>;
7
+ "aria-label"?: string | undefined;
8
+ } | {
9
+ label?: undefined;
10
+ /** A bare switch (a table cell) has no words to name it: this is its name. */
11
+ "aria-label": string;
12
+ };
13
+ type SwitchProps = Naming & {
14
+ /** Whether it is on. The caller owns it: there is no uncontrolled mode. */
15
+ checked: boolean;
16
+ /** Called with the next value when the person flips it. Not called while `busy`. */
17
+ onChange: (checked: boolean) => void;
18
+ /** A muted line under the label that says more. It is the switch's `aria-describedby`. */
19
+ description?: ReactNode;
20
+ /**
21
+ * The label's text size and the switch with it.
22
+ * @default md
23
+ */
24
+ size?: Size | undefined;
25
+ /**
26
+ * The change is being saved: `aria-busy`, and a click or a key does nothing. It keeps its colour
27
+ * and its focus, which a native `disabled` would lose when the save starts.
28
+ */
29
+ busy?: boolean | undefined;
30
+ } & Unstyled<Omit<ComponentPropsWithoutRef<"button">, "type" | "role" | "children" | "onChange" | "aria-checked" | "aria-busy" | "aria-disabled" | "aria-label">>;
31
+ /**
32
+ * An on/off switch for a setting that takes effect at once, usually by saving it to a server: a
33
+ * native `<button role="switch" aria-checked>`. It is controlled: the caller owns `checked` and
34
+ * flips it in `onChange`. A value sent with a form is a `Checkbox`, not a Switch. Give it a `label`
35
+ * (the whole label is the click target), or in a table cell leave the label out and name it with
36
+ * `aria-label`. `description` is a muted line under the label, and the switch's
37
+ * `aria-describedby`. `size` is `sm`, `md` (the default) or `lg`. `busy` is for the save in
38
+ * flight: clicks and keys do nothing, and the switch keeps its colour and its focus. `disabled` is
39
+ * the native one, washed out.
40
+ */
41
+ export declare function Switch({ label, description, size, busy, "aria-describedby": describedBy, ...rest }: SwitchProps): ReactElement;
42
+ export {};
package/dist/Switch.js ADDED
@@ -0,0 +1,56 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx, focusRing } from "./cx.js";
3
+ import { useDescription, Words } from "./labelWords.js";
4
+ /**
5
+ * The label's gap and text, the track, its thumb and the thumb's slide, by `size`. The track is a
6
+ * 2px border plus 2px of padding, so the thumb is the track's height less 8px and slides the
7
+ * track's width less 8px less its own width.
8
+ */
9
+ const SIZE = {
10
+ sm: { label: "gap-2 text-xs", track: "h-5 w-9", thumb: "h-3 w-3", slide: "translate-x-4" },
11
+ md: { label: "gap-2.5 text-sm", track: "h-6 w-11", thumb: "h-4 w-4", slide: "translate-x-5" },
12
+ lg: { label: "gap-3 text-base", track: "h-7 w-13", thumb: "h-5 w-5", slide: "translate-x-6" },
13
+ };
14
+ // Off is an outline of the text colour with a thumb of the same colour; on is the accent filled,
15
+ // with a thumb in its pale text colour. `border-current` and `bg-current` are no palette colour.
16
+ const OFF = "border-current text-slate-500";
17
+ const ON = "border-violet-600 bg-violet-600 text-violet-50";
18
+ /** `not-allowed` for a disabled switch, `progress` for a busy one, and the pointer otherwise. */
19
+ function cursorOf(disabled, busy) {
20
+ if (disabled)
21
+ return "cursor-not-allowed";
22
+ return busy ? "cursor-progress" : "cursor-pointer";
23
+ }
24
+ /** A disabled switch washes out as a Button does; a busy one is only a little dimmer. */
25
+ function washOf(disabled, busy) {
26
+ return cx(cursorOf(disabled, busy), disabled && "opacity-50", busy && "opacity-85");
27
+ }
28
+ /** The switch itself: the track, with the thumb in it. */
29
+ function Track({ checked, onChange, size, busy, disabled, onClick, className, ...rest }) {
30
+ const { track, thumb, slide } = SIZE[size];
31
+ return (_jsx("button", { type: "button", role: "switch", "aria-checked": checked, "aria-busy": busy || undefined, "aria-disabled": busy || undefined, disabled: disabled, className: cx("inline-flex shrink-0 items-center rounded-full border-2 p-0.5", track, checked ? ON : OFF, className, focusRing), onClick: (event) => {
32
+ onClick?.(event);
33
+ if (event.defaultPrevented || busy)
34
+ return;
35
+ onChange(!checked);
36
+ }, ...rest, children: _jsx("span", { "aria-hidden": "true", className: cx("block rounded-full bg-current transition-transform motion-reduce:transition-none", thumb, checked ? slide : "translate-x-0") }) }));
37
+ }
38
+ /**
39
+ * An on/off switch for a setting that takes effect at once, usually by saving it to a server: a
40
+ * native `<button role="switch" aria-checked>`. It is controlled: the caller owns `checked` and
41
+ * flips it in `onChange`. A value sent with a form is a `Checkbox`, not a Switch. Give it a `label`
42
+ * (the whole label is the click target), or in a table cell leave the label out and name it with
43
+ * `aria-label`. `description` is a muted line under the label, and the switch's
44
+ * `aria-describedby`. `size` is `sm`, `md` (the default) or `lg`. `busy` is for the save in
45
+ * flight: clicks and keys do nothing, and the switch keeps its colour and its focus. `disabled` is
46
+ * the native one, washed out.
47
+ */
48
+ export function Switch({ label, description, size = "md", busy = false, "aria-describedby": describedBy, ...rest }) {
49
+ const { descriptionId, describedIds } = useDescription(description, describedBy);
50
+ const wash = washOf(rest.disabled, busy);
51
+ const track = (_jsx(Track, { ...rest, size: size, busy: busy, className: label === undefined ? wash : cursorOf(rest.disabled, busy), ...(describedIds ? { "aria-describedby": describedIds } : {}) }));
52
+ if (label === undefined)
53
+ return track;
54
+ const start = descriptionId !== undefined;
55
+ return (_jsxs("label", { className: cx("inline-flex text-slate-700", start ? "items-start" : "items-center", SIZE[size].label, wash), children: [track, _jsx("span", { className: cx(start && "mt-0.5"), children: _jsx(Words, { label: label, description: description, id: descriptionId }) })] }));
56
+ }
@@ -1,13 +1,15 @@
1
1
  import type { ComponentProps, ReactElement, ReactNode } from "react";
2
2
  import { TableCell } from "./Table.js";
3
3
  /**
4
- * A `TableCell` whose value opens an editor in place. It shows `children` as a button with a dotted
5
- * underline; a click, Enter or Space swaps it for `renderEditor(close)` and moves the focus into the
6
- * editor. The cell keeps its own open state. The editor calls `close` when it has saved, and Esc
7
- * closes it: both put the focus back on the value. A press outside it or Tab out of it closes it too and leaves the
8
- * focus where it went, so the editor saves its value on its own change or submit, never on a close.
9
- * Put the control in the cell itself, not in a popover of its own, which counts as outside. Takes
10
- * `TableCell`'s `align`, `tone`, `numeric`, `mono`, `indent` and `secondary`.
4
+ * A `TableCell` whose value opens an editor in place: a cell that holds one `InlineEdit` and
5
+ * nothing else. It shows `children` as a button with a dotted underline; a click, Enter or Space
6
+ * swaps it for `renderEditor(close)` and moves the focus into the editor. The editor calls `close`
7
+ * when it has saved, and Esc closes it: both put the focus back on the value. A press outside it or
8
+ * Tab out of it closes it too and leaves the focus where it went, so the editor saves its value on
9
+ * its own change or submit, never on a close. Put the control in the cell itself, not in a popover
10
+ * of its own, which counts as outside. Takes `TableCell`'s `align`, `tone`, `numeric`, `mono`,
11
+ * `indent` and `secondary`. For an edit trigger in a cell that holds other content too, put an
12
+ * `InlineEdit` in a `TableCell`.
11
13
  */
12
14
  export declare function TableEditCell({ label, renderEditor, children, ...cell }: {
13
15
  /** What the button does, for a screen reader, heard after the value: "Edit category". It also names the editor. */
@@ -1,58 +1,17 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useRef, useState } from "react";
3
- import { cx, focusRing } from "./cx.js";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { InlineEdit } from "./InlineEdit.js";
4
3
  import { TableCell } from "./Table.js";
5
- /** What takes the focus when the editor opens, unless the editor already took it (`autoFocus`). */
6
- const FOCUSABLE = "input, select, textarea, button, a[href]";
7
- /** Whether `target` is a node outside `box`. */
8
- function isOutside(box, target) {
9
- return target instanceof Node && box !== null && !box.contains(target);
10
- }
11
- /** Closes the editor when a press lands outside `box`, for as long as `open`. */
12
- function useDismissOnOutsidePress(box, open, dismiss) {
13
- useEffect(() => {
14
- if (!open)
15
- return undefined;
16
- const onPress = (event) => {
17
- if (isOutside(box.current, event.target))
18
- dismiss();
19
- };
20
- document.addEventListener("pointerdown", onPress);
21
- return () => document.removeEventListener("pointerdown", onPress);
22
- }, [box, open, dismiss]);
23
- }
24
4
  /**
25
- * A `TableCell` whose value opens an editor in place. It shows `children` as a button with a dotted
26
- * underline; a click, Enter or Space swaps it for `renderEditor(close)` and moves the focus into the
27
- * editor. The cell keeps its own open state. The editor calls `close` when it has saved, and Esc
28
- * closes it: both put the focus back on the value. A press outside it or Tab out of it closes it too and leaves the
29
- * focus where it went, so the editor saves its value on its own change or submit, never on a close.
30
- * Put the control in the cell itself, not in a popover of its own, which counts as outside. Takes
31
- * `TableCell`'s `align`, `tone`, `numeric`, `mono`, `indent` and `secondary`.
5
+ * A `TableCell` whose value opens an editor in place: a cell that holds one `InlineEdit` and
6
+ * nothing else. It shows `children` as a button with a dotted underline; a click, Enter or Space
7
+ * swaps it for `renderEditor(close)` and moves the focus into the editor. The editor calls `close`
8
+ * when it has saved, and Esc closes it: both put the focus back on the value. A press outside it or
9
+ * Tab out of it closes it too and leaves the focus where it went, so the editor saves its value on
10
+ * its own change or submit, never on a close. Put the control in the cell itself, not in a popover
11
+ * of its own, which counts as outside. Takes `TableCell`'s `align`, `tone`, `numeric`, `mono`,
12
+ * `indent` and `secondary`. For an edit trigger in a cell that holds other content too, put an
13
+ * `InlineEdit` in a `TableCell`.
32
14
  */
33
15
  export function TableEditCell({ label, renderEditor, children, ...cell }) {
34
- const [phase, setPhase] = useState("closed");
35
- const open = phase === "open";
36
- const box = useRef(null);
37
- const trigger = useRef(null);
38
- const dismiss = useCallback(() => setPhase("closed"), []);
39
- const close = () => setPhase("returning");
40
- useDismissOnOutsidePress(box, open, dismiss);
41
- useEffect(() => {
42
- if (phase === "returning")
43
- trigger.current?.focus();
44
- if (phase === "open" && !box.current?.contains(document.activeElement))
45
- box.current?.querySelector(FOCUSABLE)?.focus();
46
- }, [phase]);
47
- const onKeyDown = (event) => {
48
- if (event.key !== "Escape")
49
- return;
50
- event.preventDefault();
51
- event.stopPropagation();
52
- close();
53
- };
54
- return (_jsx(TableCell, { ...cell, children: open ? (_jsx("div", { ref: box, role: "group", "aria-label": label, className: "-mx-2 -my-1", onKeyDown: onKeyDown, onBlur: (event) => {
55
- if (isOutside(event.currentTarget, event.relatedTarget))
56
- dismiss();
57
- }, children: renderEditor(close) })) : (_jsxs("button", { ref: trigger, type: "button", onClick: () => setPhase("open"), className: cx("-mx-2 -my-1 block w-[calc(100%+1rem)] cursor-pointer rounded-md px-2 py-1 [text-align:inherit] underline decoration-dotted underline-offset-4 transition hover:bg-slate-100", focusRing), children: [children, _jsxs("span", { className: "sr-only", children: [" ", label] })] })) }));
16
+ return (_jsx(TableCell, { ...cell, children: _jsx(InlineEdit, { label: label, renderEditor: renderEditor, children: children }) }));
58
17
  }
@@ -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(" ");
@@ -0,0 +1,18 @@
1
+ import { type ReactElement, type ReactNode } from "react";
2
+ /** The breakpoint a folded column opens up at: below it the column sits behind a disclosure. */
3
+ export type FoldFrom = "md" | "lg";
4
+ /**
5
+ * A side column that folds behind a native `<details>` disclosure named `label`, closed, below
6
+ * `from`, and is the same column, always open, from `from` up. The column stays mounted while it is
7
+ * folded, so state inside it is kept. Shared by `SidebarLayout` (`md`) and `SideColumn` (`lg`).
8
+ *
9
+ * From `from` up the CSS shows the content of the closed `<details>` (the first frame, and a page
10
+ * that never hydrates). But a closed `<details>` is hidden to the accessibility tree and to axe, so
11
+ * once mounted an effect also opens it for real from `from` up and closes it again below. There is
12
+ * no `open` prop, so React never fights the browser's toggle and the server markup stays closed.
13
+ */
14
+ export declare function FoldedColumn({ from, label, children, }: {
15
+ from: FoldFrom;
16
+ label: string;
17
+ children: ReactNode;
18
+ }): ReactElement;
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from "react";
3
+ import { cx, focusRing } from "./cx.js";
4
+ // Each variant is written out whole, so Tailwind sees every class.
5
+ const FOLD = {
6
+ md: {
7
+ query: "(width >= 48rem)",
8
+ content: "md:[&::details-content]:[content-visibility:visible]",
9
+ summary: "md:hidden",
10
+ body: "md:mt-0",
11
+ },
12
+ lg: {
13
+ query: "(width >= 64rem)",
14
+ content: "lg:[&::details-content]:[content-visibility:visible]",
15
+ summary: "lg:hidden",
16
+ body: "lg:mt-0",
17
+ },
18
+ };
19
+ /**
20
+ * A side column that folds behind a native `<details>` disclosure named `label`, closed, below
21
+ * `from`, and is the same column, always open, from `from` up. The column stays mounted while it is
22
+ * folded, so state inside it is kept. Shared by `SidebarLayout` (`md`) and `SideColumn` (`lg`).
23
+ *
24
+ * From `from` up the CSS shows the content of the closed `<details>` (the first frame, and a page
25
+ * that never hydrates). But a closed `<details>` is hidden to the accessibility tree and to axe, so
26
+ * once mounted an effect also opens it for real from `from` up and closes it again below. There is
27
+ * no `open` prop, so React never fights the browser's toggle and the server markup stays closed.
28
+ */
29
+ export function FoldedColumn({ from, label, children, }) {
30
+ const fold = FOLD[from];
31
+ const details = useRef(null);
32
+ useEffect(() => {
33
+ const el = details.current;
34
+ if (!el || typeof window.matchMedia !== "function")
35
+ return undefined;
36
+ const wide = window.matchMedia(fold.query);
37
+ const sync = () => {
38
+ el.open = wide.matches;
39
+ };
40
+ sync();
41
+ wide.addEventListener("change", sync);
42
+ return () => wide.removeEventListener("change", sync);
43
+ }, [fold.query]);
44
+ return (_jsxs("details", { ref: details, className: cx("group", fold.content), children: [_jsxs("summary", { className: cx("flex cursor-pointer list-none items-center justify-between rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm font-medium text-slate-900 select-none hover:bg-slate-50 [&::-webkit-details-marker]:hidden", fold.summary, focusRing), children: [label, _jsx("svg", { viewBox: "0 0 20 20", width: "16", height: "16", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", className: "shrink-0 text-slate-500 group-open:rotate-180", children: _jsx("path", { d: "M5 8l5 5 5-5" }) })] }), _jsx("div", { className: cx("mt-2", fold.body), children: children })] }));
45
+ }
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
@@ -12,6 +12,7 @@ export { Container } from "./Container.js";
12
12
  export { PageHeader, type PageHeaderProps } from "./PageHeader.js";
13
13
  export { CardGrid, CenteredLayout, DetailLayout, FormLayout, FullBleedLayout, ListLayout, ProseLayout, } from "./PageLayouts.js";
14
14
  export { SidebarLayout } from "./SidebarLayout.js";
15
+ export { SideColumn, type SideColumnWidth } from "./SideColumn.js";
15
16
  export { StickyBar } from "./StickyBar.js";
16
17
  export { ErrorPage, Forbidden, Maintenance, NotFound, Offline } from "./StatusPages.js";
17
18
  export { Cluster, ClusterGrow, type ClusterAlign, type ClusterGap, type ClusterJustify, } from "./Cluster.js";
@@ -27,10 +28,14 @@ export { Callout, type CalloutTone } from "./Callout.js";
27
28
  export { ListRow, type ListRowProps } from "./ListRow.js";
28
29
  export { OutlineRegion, RegionFrame, type RegionRect } from "./OutlineRegion.js";
29
30
  export { type CellTone, type RowTone, Table, TableBody, TableCell, TableFoot, TableGroupRow, TableHead, TableHeaderCell, TableRow, } from "./Table.js";
31
+ export { InlineEdit } from "./InlineEdit.js";
30
32
  export { TableEditCell } from "./TableEditCell.js";
31
33
  export { EmptyState } from "./EmptyState.js";
32
34
  export { Step, Steps } from "./Steps.js";
33
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";
34
39
  export { Code } from "./Code.js";
35
40
  export { CodeBlock } from "./CodeBlock.js";
36
41
  export type { CodeLanguage } from "./highlight.js";
@@ -50,6 +55,7 @@ export { Tabs, type Tab, type TabsProps } from "./Tabs.js";
50
55
  export { SegmentedControl, type Segment } from "./SegmentedControl.js";
51
56
  export { FilterGroup } from "./FilterGroup.js";
52
57
  export { Checkbox } from "./Checkbox.js";
58
+ export { Switch } from "./Switch.js";
53
59
  export { Radio, RadioGroup } from "./RadioGroup.js";
54
60
  export { FileDrop } from "./FileDrop.js";
55
61
  export { CopyButton } from "./CopyButton.js";
@@ -70,6 +76,7 @@ export { Image, type ImageAspect, type ImageProps, type ImageWidth } from "./Ima
70
76
  export { MediaTile, type MediaTileProps } from "./MediaTile.js";
71
77
  export { ScrollStrip, type ScrollStripProps } from "./ScrollStrip.js";
72
78
  export { TextLink, type TextLinkSize } from "./TextLink.js";
79
+ export { ToolCallCard } from "./ToolCallCard.js";
73
80
  export { Caption, Display, Lead, Overline, PageHeading, SectionHeading, SubsectionHeading, type SubsectionHeadingSize, Text, type TextTone, type TypographyColor, type TypographyProps, } from "./typography.js";
74
81
  export { DateField, type DateFieldProps } from "./DateField.js";
75
82
  export { fromISODate, toISODate } from "./isoDate.js";
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ export { Container } from "./Container.js";
12
12
  export { PageHeader } from "./PageHeader.js";
13
13
  export { CardGrid, CenteredLayout, DetailLayout, FormLayout, FullBleedLayout, ListLayout, ProseLayout, } from "./PageLayouts.js";
14
14
  export { SidebarLayout } from "./SidebarLayout.js";
15
+ export { SideColumn } from "./SideColumn.js";
15
16
  export { StickyBar } from "./StickyBar.js";
16
17
  export { ErrorPage, Forbidden, Maintenance, NotFound, Offline } from "./StatusPages.js";
17
18
  export { Cluster, ClusterGrow, } from "./Cluster.js";
@@ -27,10 +28,14 @@ export { Callout } from "./Callout.js";
27
28
  export { ListRow } from "./ListRow.js";
28
29
  export { OutlineRegion, RegionFrame } from "./OutlineRegion.js";
29
30
  export { Table, TableBody, TableCell, TableFoot, TableGroupRow, TableHead, TableHeaderCell, TableRow, } from "./Table.js";
31
+ export { InlineEdit } from "./InlineEdit.js";
30
32
  export { TableEditCell } from "./TableEditCell.js";
31
33
  export { EmptyState } from "./EmptyState.js";
32
34
  export { Step, Steps } from "./Steps.js";
33
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";
34
39
  export { Code } from "./Code.js";
35
40
  export { CodeBlock } from "./CodeBlock.js";
36
41
  export { CodeExample } from "./CodeExample.js";
@@ -48,6 +53,7 @@ export { Tabs } from "./Tabs.js";
48
53
  export { SegmentedControl } from "./SegmentedControl.js";
49
54
  export { FilterGroup } from "./FilterGroup.js";
50
55
  export { Checkbox } from "./Checkbox.js";
56
+ export { Switch } from "./Switch.js";
51
57
  export { Radio, RadioGroup } from "./RadioGroup.js";
52
58
  export { FileDrop } from "./FileDrop.js";
53
59
  export { CopyButton } from "./CopyButton.js";
@@ -66,6 +72,7 @@ export { Image } from "./Image.js";
66
72
  export { MediaTile } from "./MediaTile.js";
67
73
  export { ScrollStrip } from "./ScrollStrip.js";
68
74
  export { TextLink } from "./TextLink.js";
75
+ export { ToolCallCard } from "./ToolCallCard.js";
69
76
  export { Caption, Display, Lead, Overline, PageHeading, SectionHeading, SubsectionHeading, Text, } from "./typography.js";
70
77
  export { DateField } from "./DateField.js";
71
78
  export { fromISODate, toISODate } from "./isoDate.js";
@@ -0,0 +1,12 @@
1
+ import { type ReactNode } from "react";
2
+ /** The label, with its muted description under it when there is one. */
3
+ export declare function Words({ label, description, id, }: {
4
+ label: ReactNode;
5
+ description: ReactNode;
6
+ id: string | undefined;
7
+ }): ReactNode;
8
+ /** The description's id (none without a description) and the control's whole `aria-describedby`. */
9
+ export declare function useDescription(description: ReactNode, describedBy: string | undefined): {
10
+ descriptionId: string | undefined;
11
+ describedIds: string;
12
+ };
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId } from "react";
3
+ /** The label, with its muted description under it when there is one. */
4
+ export function Words({ label, description, id, }) {
5
+ if (id === undefined)
6
+ return label;
7
+ 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 })] }));
8
+ }
9
+ /** The description's id (none without a description) and the control's whole `aria-describedby`. */
10
+ export function useDescription(description, describedBy) {
11
+ const generatedId = useId();
12
+ const descriptionId = description != null && description !== false ? generatedId : undefined;
13
+ return { descriptionId, describedIds: [describedBy, descriptionId].filter(Boolean).join(" ") };
14
+ }