@hackcove/ui 0.2.4 → 0.2.6

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 (70) hide show
  1. package/README.md +65 -6
  2. package/dist/AppShell.d.ts +27 -2
  3. package/dist/AppShell.js +25 -2
  4. package/dist/Badge.d.ts +9 -0
  5. package/dist/Badge.js +10 -0
  6. package/dist/Breadcrumbs.d.ts +22 -3
  7. package/dist/Breadcrumbs.js +23 -2
  8. package/dist/Button.d.ts +18 -8
  9. package/dist/Button.js +54 -22
  10. package/dist/ButtonGroup.d.ts +14 -0
  11. package/dist/ButtonGroup.js +10 -0
  12. package/dist/ButtonLink.d.ts +20 -0
  13. package/dist/ButtonLink.js +18 -0
  14. package/dist/Callout.d.ts +10 -2
  15. package/dist/Callout.js +8 -3
  16. package/dist/Checkbox.d.ts +10 -4
  17. package/dist/Checkbox.js +8 -3
  18. package/dist/ColourSchemeToggle.d.ts +14 -3
  19. package/dist/ColourSchemeToggle.js +13 -9
  20. package/dist/DateField.d.ts +15 -2
  21. package/dist/DateField.js +8 -4
  22. package/dist/DatePicker.d.ts +1 -1
  23. package/dist/DatePicker.js +3 -3
  24. package/dist/Drawer.d.ts +29 -0
  25. package/dist/Drawer.js +26 -0
  26. package/dist/FileDrop.d.ts +10 -0
  27. package/dist/FileDrop.js +11 -0
  28. package/dist/Input.d.ts +3 -2
  29. package/dist/Input.js +5 -4
  30. package/dist/KeyValueList.d.ts +11 -4
  31. package/dist/KeyValueList.js +14 -5
  32. package/dist/ListRow.d.ts +36 -0
  33. package/dist/ListRow.js +31 -0
  34. package/dist/NavTabs.d.ts +15 -4
  35. package/dist/NavTabs.js +13 -2
  36. package/dist/PageFade.d.ts +25 -0
  37. package/dist/PageFade.js +39 -0
  38. package/dist/ProgressBar.d.ts +26 -0
  39. package/dist/ProgressBar.js +32 -0
  40. package/dist/SegmentedControl.d.ts +11 -0
  41. package/dist/SegmentedControl.js +22 -0
  42. package/dist/Select.d.ts +3 -2
  43. package/dist/Select.js +4 -4
  44. package/dist/SplitBar.d.ts +21 -0
  45. package/dist/SplitBar.js +15 -0
  46. package/dist/StatTile.d.ts +16 -0
  47. package/dist/StatTile.js +21 -0
  48. package/dist/Table.d.ts +48 -0
  49. package/dist/Table.js +42 -0
  50. package/dist/Tabs.d.ts +13 -0
  51. package/dist/Tabs.js +23 -0
  52. package/dist/TextLink.d.ts +19 -4
  53. package/dist/TextLink.js +19 -4
  54. package/dist/Textarea.d.ts +3 -2
  55. package/dist/Textarea.js +4 -4
  56. package/dist/Timeline.d.ts +21 -0
  57. package/dist/Timeline.js +19 -0
  58. package/dist/colourScheme.d.ts +11 -2
  59. package/dist/colourScheme.js +16 -3
  60. package/dist/control.d.ts +19 -2
  61. package/dist/control.js +25 -2
  62. package/dist/index.d.ts +22 -6
  63. package/dist/index.js +19 -4
  64. package/dist/link.d.ts +22 -0
  65. package/dist/link.js +11 -0
  66. package/dist/switcher.d.ts +66 -0
  67. package/dist/switcher.js +53 -0
  68. package/dist/ui.d.ts +25 -5
  69. package/dist/ui.js +51 -7
  70. package/package.json +1 -1
package/dist/Select.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { controlClass } from "./control.js";
2
+ import { CONTROL_PAD, controlClass } from "./control.js";
3
3
  import { cx } from "./cx.js";
4
- /** A native `<select>` styled like `Input`, keeping the browser's own arrow. */
5
- export function Select({ className, children, ...rest }) {
6
- return (_jsx("select", { ...rest, className: cx(controlClass, "px-3", className), children: children }));
4
+ /** A native `<select>` styled like `Input`, keeping the browser's own arrow. Takes `size` and `width` too. */
5
+ export function Select({ size = "md", width = "full", className, children, ...rest }) {
6
+ return (_jsx("select", { ...rest, className: cx(controlClass(size, width), CONTROL_PAD[size].x, className), children: children }));
7
7
  }
@@ -0,0 +1,21 @@
1
+ import type { ReactElement } from "react";
2
+ import { type BarSize, type BarTone } from "./ProgressBar.js";
3
+ /**
4
+ * A bar split between two parties, `a` on the left and `b` on the right, each sized by its share of
5
+ * `a + b`. `label` names the split for a screen reader, which hears both percentages; give `names`
6
+ * (left, right) so it hears whose is whose. Put the parties' names under the bar yourself.
7
+ */
8
+ export declare function SplitBar({ a, b, label, names, toneA, toneB, size, className, }: {
9
+ a: number;
10
+ b: number;
11
+ label: string;
12
+ /** The left and right parties, spoken before their percentages. */
13
+ names?: readonly [string, string];
14
+ /** Defaults to "negative". */
15
+ toneA?: BarTone;
16
+ /** Defaults to "positive". */
17
+ toneB?: BarTone;
18
+ /** Defaults to "lg". */
19
+ size?: BarSize;
20
+ className?: string;
21
+ }): ReactElement;
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from "./cx.js";
3
+ import { BAR_FILL, BAR_TRACK, announcedPercent, sharePercent, } from "./ProgressBar.js";
4
+ /**
5
+ * A bar split between two parties, `a` on the left and `b` on the right, each sized by its share of
6
+ * `a + b`. `label` names the split for a screen reader, which hears both percentages; give `names`
7
+ * (left, right) so it hears whose is whose. Put the parties' names under the bar yourself.
8
+ */
9
+ export function SplitBar({ a, b, label, names, toneA = "negative", toneB = "positive", size = "lg", className, }) {
10
+ const shareA = sharePercent(a, a + b);
11
+ const shareB = a + b > 0 ? 100 - shareA : 0;
12
+ const percentA = announcedPercent(a, a + b);
13
+ const percentB = a + b > 0 ? 100 - percentA : 0;
14
+ return (_jsxs("div", { role: "img", "aria-label": `${label}: ${names ? `${names[0]} ${percentA}%, ${names[1]} ${percentB}%` : `${percentA}% / ${percentB}%`}`, className: cx("flex overflow-hidden rounded-full bg-slate-100", BAR_TRACK[size], className), children: [_jsx("div", { className: BAR_FILL[toneA], style: { width: `${shareA}%` } }), _jsx("div", { className: BAR_FILL[toneB], style: { width: `${shareB}%` } })] }));
15
+ }
@@ -0,0 +1,16 @@
1
+ import type { HTMLAttributes, ReactElement, ReactNode } from "react";
2
+ /** The colour of a tile's value (and of its accent bar): plain ink, green or red. */
3
+ export type StatTileTone = "neutral" | "positive" | "negative";
4
+ /**
5
+ * One headline figure: an uppercase caption over a large tabular-nums value, with an optional
6
+ * hint line beneath. `tone` colours the value; `accent` adds a bar across the top edge in the same
7
+ * tone. Pass the value already formatted.
8
+ */
9
+ export declare function StatTile({ label, value, hint, tone, accent, className, ...rest }: {
10
+ label: ReactNode;
11
+ value: ReactNode;
12
+ hint?: ReactNode;
13
+ /** Defaults to "neutral". */
14
+ tone?: StatTileTone;
15
+ accent?: boolean;
16
+ } & Omit<HTMLAttributes<HTMLDivElement>, "children">): ReactElement;
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from "./cx.js";
3
+ import { BAR_FILL } from "./ProgressBar.js";
4
+ const VALUE = {
5
+ neutral: "text-slate-900",
6
+ positive: "text-emerald-700",
7
+ negative: "text-red-700",
8
+ };
9
+ const BAR = {
10
+ neutral: BAR_FILL.accent,
11
+ positive: BAR_FILL.positive,
12
+ negative: BAR_FILL.negative,
13
+ };
14
+ /**
15
+ * One headline figure: an uppercase caption over a large tabular-nums value, with an optional
16
+ * hint line beneath. `tone` colours the value; `accent` adds a bar across the top edge in the same
17
+ * tone. Pass the value already formatted.
18
+ */
19
+ export function StatTile({ label, value, hint, tone = "neutral", accent = false, className, ...rest }) {
20
+ return (_jsxs("div", { className: cx("relative overflow-hidden rounded-xl border border-slate-200/70 bg-white p-5 shadow-sm", className), ...rest, children: [accent && (_jsx("span", { "aria-hidden": "true", className: cx("absolute inset-x-0 top-0 h-1", BAR[tone]) })), _jsx("p", { className: "text-xs font-semibold uppercase tracking-wider text-slate-500", children: label }), _jsx("p", { className: cx("mt-2 text-3xl font-bold tabular-nums", VALUE[tone]), children: value }), hint != null && _jsx("p", { className: "mt-1 text-xs text-slate-500", children: hint })] }));
21
+ }
@@ -0,0 +1,48 @@
1
+ import type { ReactElement, ReactNode } from "react";
2
+ type Align = "start" | "end";
3
+ declare const TONE: {
4
+ readonly default: "text-slate-700";
5
+ readonly strong: "font-medium text-slate-900";
6
+ readonly muted: "text-slate-500";
7
+ };
8
+ /**
9
+ * The classes of the one link in a row whose hit area covers the row, so the whole row is the
10
+ * click target. For a router's own `<Link>`; `TableRow` is the `relative` ancestor it stretches to.
11
+ */
12
+ export declare const rowLinkClass: string;
13
+ /**
14
+ * A list of records in a rounded, bordered card that scrolls sideways on a narrow screen. Compose it
15
+ * from `TableHead`, `TableBody`, `TableRow`, `TableHeaderCell` and `TableCell`. `caption` names the
16
+ * table for assistive tech and is not drawn.
17
+ */
18
+ export declare function Table({ caption, className, children, }: {
19
+ caption?: string | undefined;
20
+ className?: string | undefined;
21
+ children: ReactNode;
22
+ }): ReactElement;
23
+ /** The `<thead>`: small uppercase column titles on a tinted strip. Holds one `TableRow` of `TableHeaderCell`s. */
24
+ export declare function TableHead({ children }: {
25
+ children: ReactNode;
26
+ }): ReactElement;
27
+ /** The `<tbody>`: the rows, each with a line above it and a fill on hover. */
28
+ export declare function TableBody({ children }: {
29
+ children: ReactNode;
30
+ }): ReactElement;
31
+ /** A `<tr>`, `relative`: the anchor a `rowLinkClass` link stretches to. Use it in the head and the body. */
32
+ export declare function TableRow({ children }: {
33
+ children: ReactNode;
34
+ }): ReactElement;
35
+ /** A column title, `<th scope="col">`. */
36
+ export declare function TableHeaderCell({ align, children, }: {
37
+ align?: Align | undefined;
38
+ children?: ReactNode;
39
+ }): ReactElement;
40
+ /** A `<td>`. `tone` is the text weight and colour; `numeric` sets tabular figures, `mono` a small mono face. */
41
+ export declare function TableCell({ align, tone, numeric, mono, children, }: {
42
+ align?: Align | undefined;
43
+ tone?: keyof typeof TONE | undefined;
44
+ numeric?: boolean | undefined;
45
+ mono?: boolean | undefined;
46
+ children?: ReactNode;
47
+ }): ReactElement;
48
+ export {};
package/dist/Table.js ADDED
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from "./cx.js";
3
+ import { linkClass, linkStretchClass } from "./TextLink.js";
4
+ const ALIGN = { start: "text-left", end: "text-right" };
5
+ const TONE = {
6
+ default: "text-slate-700",
7
+ strong: "font-medium text-slate-900",
8
+ muted: "text-slate-500",
9
+ };
10
+ /**
11
+ * The classes of the one link in a row whose hit area covers the row, so the whole row is the
12
+ * click target. For a router's own `<Link>`; `TableRow` is the `relative` ancestor it stretches to.
13
+ */
14
+ export const rowLinkClass = cx(linkClass, linkStretchClass);
15
+ /**
16
+ * A list of records in a rounded, bordered card that scrolls sideways on a narrow screen. Compose it
17
+ * from `TableHead`, `TableBody`, `TableRow`, `TableHeaderCell` and `TableCell`. `caption` names the
18
+ * table for assistive tech and is not drawn.
19
+ */
20
+ export function Table({ caption, className, children, }) {
21
+ return (_jsx("div", { className: cx("overflow-x-auto rounded-xl border border-slate-200/70 bg-white shadow-sm", className), children: _jsxs("table", { className: "w-full text-left text-sm", children: [caption && _jsx("caption", { className: "sr-only", children: caption }), children] }) }));
22
+ }
23
+ /** The `<thead>`: small uppercase column titles on a tinted strip. Holds one `TableRow` of `TableHeaderCell`s. */
24
+ export function TableHead({ children }) {
25
+ return (_jsx("thead", { className: "bg-slate-50 text-xs uppercase tracking-wide text-slate-500", children: children }));
26
+ }
27
+ /** The `<tbody>`: the rows, each with a line above it and a fill on hover. */
28
+ export function TableBody({ children }) {
29
+ return (_jsx("tbody", { className: "[&>tr:hover]:bg-slate-50 [&>tr]:border-t [&>tr]:border-slate-200/70", children: children }));
30
+ }
31
+ /** A `<tr>`, `relative`: the anchor a `rowLinkClass` link stretches to. Use it in the head and the body. */
32
+ export function TableRow({ children }) {
33
+ return _jsx("tr", { className: "relative", children: children });
34
+ }
35
+ /** A column title, `<th scope="col">`. */
36
+ export function TableHeaderCell({ align = "start", children, }) {
37
+ return (_jsx("th", { scope: "col", className: cx("whitespace-nowrap px-4 py-2.5 font-medium", ALIGN[align]), children: children }));
38
+ }
39
+ /** A `<td>`. `tone` is the text weight and colour; `numeric` sets tabular figures, `mono` a small mono face. */
40
+ export function TableCell({ align = "start", tone = "default", numeric = false, mono = false, children, }) {
41
+ return (_jsx("td", { className: cx("whitespace-nowrap px-4 py-3", ALIGN[align], TONE[tone], numeric && "tabular-nums", mono && "font-mono text-xs"), children: children }));
42
+ }
package/dist/Tabs.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type { ReactElement } from "react";
2
+ import { type SwitchItem, type SwitchProps } from "./switcher.js";
3
+ /** One tab of a Tabs row. See `SwitchItem`. */
4
+ export type Tab = SwitchItem;
5
+ /**
6
+ * Underline tabs for switching a view in place: a row over a bottom rule, the current tab
7
+ * underlined in the accent. `items` are `{ value, label }` buttons (`value` and `onChange` are the
8
+ * caller's state; the arrow keys, Home and End move between tabs) or links (`href`, or `render` to
9
+ * draw the app's own router `<Link>`; `value` marks the current one, which gets `aria-current`).
10
+ * `label` names the tab list. The panel is the caller's: give it `role="tabpanel"`, and a tab
11
+ * `controls` to point at it.
12
+ */
13
+ export declare function Tabs(props: SwitchProps): ReactElement;
package/dist/Tabs.js ADDED
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cx, focusRing } from "./cx.js";
3
+ import { Switcher } from "./switcher.js";
4
+ const BASE = `-mb-px border-b-2 px-0.5 py-2 transition ${focusRing}`;
5
+ const ON = "border-violet-600 font-medium text-violet-700";
6
+ const OFF = "border-transparent text-slate-600 hover:text-violet-700";
7
+ /**
8
+ * Underline tabs for switching a view in place: a row over a bottom rule, the current tab
9
+ * underlined in the accent. `items` are `{ value, label }` buttons (`value` and `onChange` are the
10
+ * caller's state; the arrow keys, Home and End move between tabs) or links (`href`, or `render` to
11
+ * draw the app's own router `<Link>`; `value` marks the current one, which gets `aria-current`).
12
+ * `label` names the tab list. The panel is the caller's: give it `role="tabpanel"`, and a tab
13
+ * `controls` to point at it.
14
+ */
15
+ export function Tabs(props) {
16
+ return (_jsx(Switcher, { ...props, parts: {
17
+ group: "tablist",
18
+ item: "tab",
19
+ vertical: false,
20
+ root: "flex flex-wrap gap-x-5 border-b border-slate-200 text-sm",
21
+ option: (active) => cx(BASE, active ? ON : OFF),
22
+ } }));
23
+ }
@@ -1,12 +1,27 @@
1
1
  import type { ComponentPropsWithoutRef, ReactElement } from "react";
2
+ import { type LinkRender } from "./link.js";
2
3
  /** The classes of an inline text link, for a router's own <Link> that should look like one. */
3
4
  export declare const linkClass: string;
5
+ /**
6
+ * Stretches a link's hit area over its nearest `relative` ancestor (a table row, a card), so the
7
+ * whole of it opens the link. Add it to the one link in the row; the ancestor needs `relative`.
8
+ */
9
+ export declare const linkStretchClass = "after:absolute after:inset-0";
4
10
  type TextLinkProps = {
5
11
  /** Opens in a new tab: adds `target="_blank"` and `rel="noreferrer"`. */
6
12
  external?: boolean | undefined;
7
- } & ComponentPropsWithoutRef<"a"> & {
13
+ /** The link covers its nearest `relative` ancestor, so the whole row or card is the click target. */
14
+ stretch?: boolean | undefined;
15
+ } & Omit<ComponentPropsWithoutRef<"a">, "href"> & ({
8
16
  href: string;
9
- };
10
- /** An inline text link in the accent colour. Every anchor prop goes on the `<a>`. */
11
- export declare function TextLink({ external, className, children, ...rest }: TextLinkProps): ReactElement;
17
+ render?: LinkRender | undefined;
18
+ } | {
19
+ href?: string | undefined;
20
+ render: LinkRender;
21
+ });
22
+ /**
23
+ * An inline text link in the accent colour. Every anchor prop goes on the `<a>`. With `render` it
24
+ * draws the app's own router `<Link>` instead (`render={(p) => <Link to="/terms" {...p} />}`).
25
+ */
26
+ export declare function TextLink({ external, stretch, href, render, className, children, ...rest }: TextLinkProps): ReactElement;
12
27
  export {};
package/dist/TextLink.js CHANGED
@@ -1,8 +1,23 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
1
  import { cx, focusRing } from "./cx.js";
2
+ import { renderLink } from "./link.js";
3
3
  /** The classes of an inline text link, for a router's own <Link> that should look like one. */
4
4
  export const linkClass = cx("rounded font-medium text-violet-600 underline-offset-2 transition-colors hover:text-violet-700", focusRing);
5
- /** An inline text link in the accent colour. Every anchor prop goes on the `<a>`. */
6
- export function TextLink({ external = false, className, children, ...rest }) {
7
- return (_jsx("a", { className: cx(linkClass, className), ...(external ? { target: "_blank", rel: "noreferrer" } : {}), ...rest, children: children }));
5
+ /**
6
+ * Stretches a link's hit area over its nearest `relative` ancestor (a table row, a card), so the
7
+ * whole of it opens the link. Add it to the one link in the row; the ancestor needs `relative`.
8
+ */
9
+ export const linkStretchClass = "after:absolute after:inset-0";
10
+ /**
11
+ * An inline text link in the accent colour. Every anchor prop goes on the `<a>`. With `render` it
12
+ * draws the app's own router `<Link>` instead (`render={(p) => <Link to="/terms" {...p} />}`).
13
+ */
14
+ export function TextLink({ external = false, stretch = false, href, render, className, children, ...rest }) {
15
+ return renderLink({
16
+ ...rest,
17
+ href,
18
+ render,
19
+ external,
20
+ className: cx(linkClass, stretch && linkStretchClass, className),
21
+ children,
22
+ });
8
23
  }
@@ -1,3 +1,4 @@
1
1
  import type { ComponentPropsWithoutRef, ReactElement } from "react";
2
- /** A multi-line text input, styled like `Input`. `rows` defaults to 3. */
3
- export declare function Textarea({ className, ...rest }: ComponentPropsWithoutRef<"textarea">): ReactElement;
2
+ import { type ControlProps } from "./control.js";
3
+ /** A multi-line text input, styled like `Input`. `rows` defaults to 3. Takes `size` and `width` too. */
4
+ export declare function Textarea({ size, width, className, ...rest }: ControlProps & Omit<ComponentPropsWithoutRef<"textarea">, "width">): ReactElement;
package/dist/Textarea.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { controlClass } from "./control.js";
2
+ import { CONTROL_PAD, controlClass } from "./control.js";
3
3
  import { cx } from "./cx.js";
4
- /** A multi-line text input, styled like `Input`. `rows` defaults to 3. */
5
- export function Textarea({ className, ...rest }) {
6
- return _jsx("textarea", { rows: 3, ...rest, className: cx(controlClass, "px-3", className) });
4
+ /** A multi-line text input, styled like `Input`. `rows` defaults to 3. Takes `size` and `width` too. */
5
+ export function Textarea({ size = "md", width = "full", className, ...rest }) {
6
+ return (_jsx("textarea", { rows: 3, ...rest, className: cx(controlClass(size, width), CONTROL_PAD[size].x, className) }));
7
7
  }
@@ -0,0 +1,21 @@
1
+ import { type ReactElement, type ReactNode } from "react";
2
+ /**
3
+ * A vertical list of events, newest or oldest first as the caller orders them. A named `<ol>` of
4
+ * `TimelineItem`s on a rail; with no children it draws `empty` instead of an empty list.
5
+ */
6
+ export declare function Timeline({ label, empty, children, }: {
7
+ /** Names the list (`aria-label`), e.g. "Status history". */
8
+ label: string;
9
+ /** Drawn in place of the list when there are no items. */
10
+ empty?: ReactNode;
11
+ children?: ReactNode;
12
+ }): ReactElement | null;
13
+ /**
14
+ * One event: a `title` (a `Pill`, a heading), a `time` the app has already formatted, and the
15
+ * `children` as a note under them.
16
+ */
17
+ export declare function TimelineItem({ title, time, children, }: {
18
+ title: ReactNode;
19
+ time?: ReactNode;
20
+ children?: ReactNode;
21
+ }): ReactElement;
@@ -0,0 +1,19 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Children } from "react";
3
+ /**
4
+ * A vertical list of events, newest or oldest first as the caller orders them. A named `<ol>` of
5
+ * `TimelineItem`s on a rail; with no children it draws `empty` instead of an empty list.
6
+ */
7
+ export function Timeline({ label, empty, children, }) {
8
+ if (Children.toArray(children).length === 0) {
9
+ return empty == null ? null : _jsx(_Fragment, { children: empty });
10
+ }
11
+ return (_jsx("ol", { "aria-label": label, className: "flex flex-col", children: children }));
12
+ }
13
+ /**
14
+ * One event: a `title` (a `Pill`, a heading), a `time` the app has already formatted, and the
15
+ * `children` as a note under them.
16
+ */
17
+ export function TimelineItem({ title, time, children, }) {
18
+ return (_jsxs("li", { className: "relative border-l border-slate-300 pb-5 pl-5 last:border-transparent last:pb-0", children: [_jsx("span", { "aria-hidden": "true", className: "absolute -left-[5px] top-1.5 h-2.5 w-2.5 rounded-full border-2 border-slate-300 bg-white" }), _jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-1", children: [_jsx("span", { className: "text-sm font-medium text-slate-900", children: title }), time != null && _jsx("span", { className: "text-xs tabular-nums text-slate-500", children: time })] }), children != null && _jsx("div", { className: "mt-1 text-sm text-slate-700", children: children })] }));
19
+ }
@@ -1,6 +1,15 @@
1
1
  export type ColourScheme = "light" | "dark";
2
- /** Put `scheme` on `root` (the `<html>` element by default): the `dark` class, and `color-scheme` so native controls and scrollbars follow. */
3
- export declare function applyColourScheme(scheme: ColourScheme, root?: HTMLElement): void;
2
+ /** The class each scheme puts on `<html>`: none for light and `dark` for dark, unless the app's theme says otherwise (a light theme with a scope class of its own). */
3
+ export type ColourSchemeClasses = {
4
+ light: string;
5
+ dark: string;
6
+ };
7
+ /** The class for `scheme`: what to render on `<html>` on the server so the first paint is right. `""` for the default light scheme. */
8
+ export declare function schemeClass(scheme: ColourScheme, schemes?: ColourSchemeClasses): string;
9
+ /** Which scheme `root` is in: dark when it carries the dark class, else light. */
10
+ export declare function appliedColourScheme(root?: HTMLElement, schemes?: ColourSchemeClasses): ColourScheme;
11
+ /** Put `scheme` on `root` (the `<html>` element by default): its class from `schemes` (default `dark` for dark and none for light), the other scheme's class taken off, and `color-scheme` so native controls and scrollbars follow. */
12
+ export declare function applyColourScheme(scheme: ColourScheme, root?: HTMLElement, schemes?: ColourSchemeClasses): void;
4
13
  /** The stored choice, else the system preference. `fallback` (default `"light"`) is for when neither is available: server rendering, or storage that throws with no `matchMedia`. */
5
14
  export declare function readColourScheme(fallback?: ColourScheme): ColourScheme;
6
15
  /** Remember `scheme` for the next visit. A storage error is swallowed, so private mode does not crash. */
@@ -1,9 +1,22 @@
1
1
  const STORAGE_KEY = "hackcove:colour-scheme";
2
2
  const DARK_QUERY = "(prefers-color-scheme: dark)";
3
3
  const isScheme = (value) => value === "light" || value === "dark";
4
- /** Put `scheme` on `root` (the `<html>` element by default): the `dark` class, and `color-scheme` so native controls and scrollbars follow. */
5
- export function applyColourScheme(scheme, root = document.documentElement) {
6
- root.classList.toggle("dark", scheme === "dark");
4
+ const DEFAULT_CLASSES = { light: "", dark: "dark" };
5
+ /** The class for `scheme`: what to render on `<html>` on the server so the first paint is right. `""` for the default light scheme. */
6
+ export function schemeClass(scheme, schemes = DEFAULT_CLASSES) {
7
+ return schemes[scheme];
8
+ }
9
+ /** Which scheme `root` is in: dark when it carries the dark class, else light. */
10
+ export function appliedColourScheme(root = document.documentElement, schemes = DEFAULT_CLASSES) {
11
+ return schemes.dark !== "" && root.classList.contains(schemes.dark) ? "dark" : "light";
12
+ }
13
+ /** Put `scheme` on `root` (the `<html>` element by default): its class from `schemes` (default `dark` for dark and none for light), the other scheme's class taken off, and `color-scheme` so native controls and scrollbars follow. */
14
+ export function applyColourScheme(scheme, root = document.documentElement, schemes = DEFAULT_CLASSES) {
15
+ const other = scheme === "dark" ? "light" : "dark";
16
+ if (schemes[other] !== "")
17
+ root.classList.remove(schemes[other]);
18
+ if (schemes[scheme] !== "")
19
+ root.classList.add(schemes[scheme]);
7
20
  root.style.colorScheme = scheme;
8
21
  }
9
22
  function storedScheme() {
package/dist/control.d.ts CHANGED
@@ -1,2 +1,19 @@
1
- /** Shared look of text controls; each control adds its own horizontal padding. */
2
- export declare const controlClass = "w-full rounded-lg border border-slate-300 bg-white py-2 text-sm text-slate-900 placeholder:text-slate-400 transition hover:border-violet-400 disabled:opacity-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-600";
1
+ export type ControlSize = "md" | "sm";
2
+ /** `full` fills the parent (the default); `auto` is as wide as the content, or as wide as a `className` width says; the rest are fixed. */
3
+ export type ControlWidth = "full" | "auto" | "xs" | "sm" | "md" | "lg";
4
+ /** The props `Input`, `Select` and `Textarea` share. They replace the native `size` and `width`. */
5
+ export type ControlProps = {
6
+ /** `sm` is compact, `text-xs` with less padding, for dense forms and table rows. */
7
+ size?: ControlSize | undefined;
8
+ /** `full` (default) fills the parent; `xs`, `sm`, `md`, `lg` are fixed; `auto` is as wide as its content, or its `className` width. */
9
+ width?: ControlWidth | undefined;
10
+ };
11
+ /** One width class per control, so a fixed width never fights `w-full`. `auto` has none. */
12
+ export declare const CONTROL_WIDTH: Record<ControlWidth, string | undefined>;
13
+ /** The horizontal padding of a control, and of an `Input`'s icon gutter. */
14
+ export declare const CONTROL_PAD: Record<ControlSize, {
15
+ x: string;
16
+ icon: string;
17
+ }>;
18
+ /** Shared look of text controls; `size` sets the height and type, `width` the one width class. */
19
+ export declare function controlClass(size?: ControlSize, width?: ControlWidth): string;
package/dist/control.js CHANGED
@@ -1,3 +1,26 @@
1
1
  import { focusRing } from "./cx.js";
2
- /** Shared look of text controls; each control adds its own horizontal padding. */
3
- export const controlClass = `w-full rounded-lg border border-slate-300 bg-white py-2 text-sm text-slate-900 placeholder:text-slate-400 transition hover:border-violet-400 disabled:opacity-50 ${focusRing}`;
2
+ const SIZE = { md: "py-2 text-sm", sm: "py-1 text-xs" };
3
+ /** One width class per control, so a fixed width never fights `w-full`. `auto` has none. */
4
+ export const CONTROL_WIDTH = {
5
+ full: "w-full",
6
+ auto: undefined,
7
+ xs: "w-20",
8
+ sm: "w-28",
9
+ md: "w-36",
10
+ lg: "w-48",
11
+ };
12
+ /** The horizontal padding of a control, and of an `Input`'s icon gutter. */
13
+ export const CONTROL_PAD = {
14
+ md: { x: "px-3", icon: "pr-3 pl-9" },
15
+ sm: { x: "px-2", icon: "pr-2 pl-7" },
16
+ };
17
+ /** Shared look of text controls; `size` sets the height and type, `width` the one width class. */
18
+ export function controlClass(size = "md", width = "full") {
19
+ return [
20
+ CONTROL_WIDTH[width],
21
+ SIZE[size],
22
+ `rounded-lg border border-slate-300 bg-white text-slate-900 placeholder:text-slate-400 transition hover:border-violet-400 disabled:opacity-50 ${focusRing}`,
23
+ ]
24
+ .filter(Boolean)
25
+ .join(" ");
26
+ }
package/dist/index.d.ts CHANGED
@@ -1,37 +1,53 @@
1
- export { Card, EpicHeader, Empty, PageTitle, Pill, groupByEpic } from "./ui.js";
1
+ export { Card, EpicHeader, Empty, PageTitle, Pill, groupByEpic, type PillTone } from "./ui.js";
2
+ export { Badge } from "./Badge.js";
3
+ export { StatTile, type StatTileTone } from "./StatTile.js";
4
+ export { ProgressBar, type BarSize, type BarTone } from "./ProgressBar.js";
5
+ export { SplitBar } from "./SplitBar.js";
2
6
  export { Markdown } from "./Markdown.js";
3
- export { AppShell, appShellRootClass } from "./AppShell.js";
7
+ export { AppShell, AppShellBrand, appShellRootClass, appShellRootClassFor } from "./AppShell.js";
4
8
  export { Container } from "./Container.js";
5
9
  export { PageHeader, type PageHeaderProps } from "./PageHeader.js";
6
10
  export { CardGrid, CenteredLayout, DetailLayout, FormLayout, FullBleedLayout, ListLayout, ProseLayout, SidebarLayout, } from "./PageLayouts.js";
7
11
  export { Section, Stack, type StackGap, type StackWidth } from "./Stack.js";
8
- export { Button, buttonClass, type ButtonVariant } from "./Button.js";
12
+ export { Button, buttonClass, type ButtonLook, type ButtonSize, type ButtonTone, type ButtonVariant, } from "./Button.js";
13
+ export { ButtonGroup } from "./ButtonGroup.js";
14
+ export { ButtonLink } from "./ButtonLink.js";
15
+ export { type LinkRender, type LinkRenderProps } from "./link.js";
9
16
  export { Avatar, avatarColourClass, initialsOf, type AvatarSize } from "./Avatar.js";
10
- export { Breadcrumbs, crumbLinkClass } from "./Breadcrumbs.js";
17
+ export { Breadcrumbs, crumbLinkClass, type Crumb } from "./Breadcrumbs.js";
11
18
  export { BulletList } from "./BulletList.js";
12
19
  export { Callout, type CalloutTone } from "./Callout.js";
20
+ export { ListRow, type ListRowProps } from "./ListRow.js";
21
+ export { rowLinkClass, Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, } from "./Table.js";
22
+ export { Timeline, TimelineItem } from "./Timeline.js";
13
23
  export { Code } from "./Code.js";
14
24
  export { Divider } from "./Divider.js";
25
+ export { PageFade, startPageTransition } from "./PageFade.js";
15
26
  export { Dialog, type DialogProps } from "./Dialog.js";
27
+ export { Drawer, type DrawerProps } from "./Drawer.js";
16
28
  export { IconButton } from "./IconButton.js";
17
29
  export { ColourSchemeToggle } from "./ColourSchemeToggle.js";
18
- export { applyColourScheme, colourSchemeScript, readColourScheme, storeColourScheme, type ColourScheme, } from "./colourScheme.js";
30
+ export { applyColourScheme, schemeClass, type ColourSchemeClasses, colourSchemeScript, readColourScheme, storeColourScheme, type ColourScheme, } from "./colourScheme.js";
19
31
  export { NavTabs, navTabClass, type NavTab } from "./NavTabs.js";
32
+ export { Tabs, type Tab } from "./Tabs.js";
33
+ export { SegmentedControl, type Segment } from "./SegmentedControl.js";
20
34
  export { ToggleChip } from "./ToggleChip.js";
21
35
  export { FilterGroup } from "./FilterGroup.js";
22
36
  export { Checkbox } from "./Checkbox.js";
37
+ export { FileDrop } from "./FileDrop.js";
23
38
  export { CopyButton } from "./CopyButton.js";
24
39
  export { CopyLine } from "./CopyLine.js";
25
40
  export { Field } from "./Field.js";
26
41
  export { FieldDialog, type FieldDialogProps } from "./FieldDialog.js";
27
42
  export { Input } from "./Input.js";
43
+ export type { ControlSize, ControlWidth } from "./control.js";
28
44
  export { InlineLinks, type InlineLinksSeparator } from "./InlineLinks.js";
29
45
  export { Kbd } from "./Kbd.js";
30
46
  export { KeyValue, KeyValueList } from "./KeyValueList.js";
31
47
  export { Select } from "./Select.js";
32
48
  export { Textarea } from "./Textarea.js";
33
49
  export { Video } from "./Video.js";
34
- export { linkClass, TextLink } from "./TextLink.js";
50
+ export { linkClass, linkStretchClass, TextLink } from "./TextLink.js";
35
51
  export { Body, BodyStrong, Caption, Display, Lead, ModalHeading, Overline, PageHeading, SectionHeading, SubsectionHeading, type SubsectionHeadingSize, Subtitle, type TypographyColor, type TypographyProps, } from "./typography.js";
36
52
  export { DateField, type DateFieldProps } from "./DateField.js";
37
53
  export { DatePicker, type DatePickerProps } from "./DatePicker.js";
package/dist/index.js CHANGED
@@ -1,25 +1,40 @@
1
1
  export { Card, EpicHeader, Empty, PageTitle, Pill, groupByEpic } from "./ui.js";
2
+ export { Badge } from "./Badge.js";
3
+ export { StatTile } from "./StatTile.js";
4
+ export { ProgressBar } from "./ProgressBar.js";
5
+ export { SplitBar } from "./SplitBar.js";
2
6
  export { Markdown } from "./Markdown.js";
3
- export { AppShell, appShellRootClass } from "./AppShell.js";
7
+ export { AppShell, AppShellBrand, appShellRootClass, appShellRootClassFor } from "./AppShell.js";
4
8
  export { Container } from "./Container.js";
5
9
  export { PageHeader } from "./PageHeader.js";
6
10
  export { CardGrid, CenteredLayout, DetailLayout, FormLayout, FullBleedLayout, ListLayout, ProseLayout, SidebarLayout, } from "./PageLayouts.js";
7
11
  export { Section, Stack } from "./Stack.js";
8
- export { Button, buttonClass } from "./Button.js";
12
+ export { Button, buttonClass, } from "./Button.js";
13
+ export { ButtonGroup } from "./ButtonGroup.js";
14
+ export { ButtonLink } from "./ButtonLink.js";
15
+ export {} from "./link.js";
9
16
  export { Avatar, avatarColourClass, initialsOf } from "./Avatar.js";
10
17
  export { Breadcrumbs, crumbLinkClass } from "./Breadcrumbs.js";
11
18
  export { BulletList } from "./BulletList.js";
12
19
  export { Callout } from "./Callout.js";
20
+ export { ListRow } from "./ListRow.js";
21
+ export { rowLinkClass, Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, } from "./Table.js";
22
+ export { Timeline, TimelineItem } from "./Timeline.js";
13
23
  export { Code } from "./Code.js";
14
24
  export { Divider } from "./Divider.js";
25
+ export { PageFade, startPageTransition } from "./PageFade.js";
15
26
  export { Dialog } from "./Dialog.js";
27
+ export { Drawer } from "./Drawer.js";
16
28
  export { IconButton } from "./IconButton.js";
17
29
  export { ColourSchemeToggle } from "./ColourSchemeToggle.js";
18
- export { applyColourScheme, colourSchemeScript, readColourScheme, storeColourScheme, } from "./colourScheme.js";
30
+ export { applyColourScheme, schemeClass, colourSchemeScript, readColourScheme, storeColourScheme, } from "./colourScheme.js";
19
31
  export { NavTabs, navTabClass } from "./NavTabs.js";
32
+ export { Tabs } from "./Tabs.js";
33
+ export { SegmentedControl } from "./SegmentedControl.js";
20
34
  export { ToggleChip } from "./ToggleChip.js";
21
35
  export { FilterGroup } from "./FilterGroup.js";
22
36
  export { Checkbox } from "./Checkbox.js";
37
+ export { FileDrop } from "./FileDrop.js";
23
38
  export { CopyButton } from "./CopyButton.js";
24
39
  export { CopyLine } from "./CopyLine.js";
25
40
  export { Field } from "./Field.js";
@@ -31,7 +46,7 @@ export { KeyValue, KeyValueList } from "./KeyValueList.js";
31
46
  export { Select } from "./Select.js";
32
47
  export { Textarea } from "./Textarea.js";
33
48
  export { Video } from "./Video.js";
34
- export { linkClass, TextLink } from "./TextLink.js";
49
+ export { linkClass, linkStretchClass, TextLink } from "./TextLink.js";
35
50
  export { Body, BodyStrong, Caption, Display, Lead, ModalHeading, Overline, PageHeading, SectionHeading, SubsectionHeading, Subtitle, } from "./typography.js";
36
51
  export { DateField } from "./DateField.js";
37
52
  export { DatePicker } from "./DatePicker.js";
package/dist/link.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from "react";
2
+ /**
3
+ * What a `render` function receives to draw a link with the app's own router `<Link>`: the
4
+ * component's classes, its label as `children`, and (for a tab that is current or a link that is
5
+ * external) `aria-current`, `target` and `rel`. Spread it on the router's link:
6
+ * `render: (props) => <Link to="/quotes" {...props} />`. `@hackcove/ui` has no router dependency.
7
+ */
8
+ export type LinkRenderProps = Omit<ComponentPropsWithoutRef<"a">, "href" | "className" | "children"> & {
9
+ className: string;
10
+ children: ReactNode;
11
+ };
12
+ /** Draws one link. The component hands it `LinkRenderProps`; it returns the app's own link element. */
13
+ export type LinkRender = (props: LinkRenderProps) => ReactNode;
14
+ /**
15
+ * A link drawn by `render` when there is one, otherwise a plain `<a href>`. `external` adds
16
+ * `target="_blank"` and `rel="noreferrer"` to either, unless the caller passes its own.
17
+ */
18
+ export declare function renderLink({ href, render, external, ...props }: LinkRenderProps & {
19
+ href?: string | undefined;
20
+ render?: LinkRender | undefined;
21
+ external?: boolean | undefined;
22
+ }): ReactElement;
package/dist/link.js ADDED
@@ -0,0 +1,11 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * A link drawn by `render` when there is one, otherwise a plain `<a href>`. `external` adds
4
+ * `target="_blank"` and `rel="noreferrer"` to either, unless the caller passes its own.
5
+ */
6
+ export function renderLink({ href, render, external = false, ...props }) {
7
+ const all = external ? { target: "_blank", rel: "noreferrer", ...props } : props;
8
+ if (render)
9
+ return _jsx(_Fragment, { children: render(all) });
10
+ return _jsx("a", { ...(href === undefined ? {} : { href }), ...all });
11
+ }