@hackcove/ui 0.2.3 → 0.2.5

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 (91) hide show
  1. package/README.md +83 -13
  2. package/dist/AppShell.d.ts +19 -4
  3. package/dist/AppShell.js +45 -3
  4. package/dist/Avatar.d.ts +9 -4
  5. package/dist/Avatar.js +9 -3
  6. package/dist/Badge.d.ts +9 -0
  7. package/dist/Badge.js +10 -0
  8. package/dist/Breadcrumbs.d.ts +22 -3
  9. package/dist/Breadcrumbs.js +23 -2
  10. package/dist/BulletList.d.ts +3 -0
  11. package/dist/BulletList.js +6 -0
  12. package/dist/Button.d.ts +18 -8
  13. package/dist/Button.js +54 -22
  14. package/dist/ButtonGroup.d.ts +14 -0
  15. package/dist/ButtonGroup.js +10 -0
  16. package/dist/ButtonLink.d.ts +20 -0
  17. package/dist/ButtonLink.js +18 -0
  18. package/dist/Callout.d.ts +10 -2
  19. package/dist/Callout.js +8 -3
  20. package/dist/Checkbox.d.ts +10 -4
  21. package/dist/Checkbox.js +8 -3
  22. package/dist/Code.d.ts +3 -0
  23. package/dist/Code.js +6 -0
  24. package/dist/ColourSchemeToggle.d.ts +14 -3
  25. package/dist/ColourSchemeToggle.js +13 -9
  26. package/dist/Container.d.ts +3 -1
  27. package/dist/Container.js +2 -2
  28. package/dist/DateField.d.ts +15 -2
  29. package/dist/DateField.js +8 -4
  30. package/dist/DatePicker.d.ts +1 -1
  31. package/dist/DatePicker.js +3 -3
  32. package/dist/Divider.d.ts +3 -0
  33. package/dist/Divider.js +6 -0
  34. package/dist/Drawer.d.ts +29 -0
  35. package/dist/Drawer.js +26 -0
  36. package/dist/FileDrop.d.ts +10 -0
  37. package/dist/FileDrop.js +11 -0
  38. package/dist/InlineLinks.d.ts +11 -0
  39. package/dist/InlineLinks.js +11 -0
  40. package/dist/Input.d.ts +3 -2
  41. package/dist/Input.js +5 -4
  42. package/dist/KeyValueList.d.ts +11 -4
  43. package/dist/KeyValueList.js +14 -5
  44. package/dist/ListRow.d.ts +36 -0
  45. package/dist/ListRow.js +31 -0
  46. package/dist/Markdown.js +1 -1
  47. package/dist/NavTabs.d.ts +15 -4
  48. package/dist/NavTabs.js +13 -2
  49. package/dist/PageFade.d.ts +25 -0
  50. package/dist/PageFade.js +39 -0
  51. package/dist/PageLayouts.d.ts +22 -1
  52. package/dist/PageLayouts.js +20 -2
  53. package/dist/ProgressBar.d.ts +26 -0
  54. package/dist/ProgressBar.js +32 -0
  55. package/dist/SegmentedControl.d.ts +11 -0
  56. package/dist/SegmentedControl.js +22 -0
  57. package/dist/Select.d.ts +3 -2
  58. package/dist/Select.js +4 -4
  59. package/dist/SplitBar.d.ts +21 -0
  60. package/dist/SplitBar.js +15 -0
  61. package/dist/Stack.d.ts +18 -6
  62. package/dist/Stack.js +20 -6
  63. package/dist/StatTile.d.ts +16 -0
  64. package/dist/StatTile.js +21 -0
  65. package/dist/Table.d.ts +48 -0
  66. package/dist/Table.js +42 -0
  67. package/dist/Tabs.d.ts +13 -0
  68. package/dist/Tabs.js +23 -0
  69. package/dist/TextLink.d.ts +27 -0
  70. package/dist/TextLink.js +23 -0
  71. package/dist/Textarea.d.ts +3 -2
  72. package/dist/Textarea.js +4 -4
  73. package/dist/Timeline.d.ts +21 -0
  74. package/dist/Timeline.js +19 -0
  75. package/dist/Video.d.ts +10 -0
  76. package/dist/Video.js +10 -0
  77. package/dist/colourScheme.d.ts +11 -2
  78. package/dist/colourScheme.js +16 -3
  79. package/dist/control.d.ts +19 -2
  80. package/dist/control.js +25 -2
  81. package/dist/index.d.ts +29 -7
  82. package/dist/index.js +24 -3
  83. package/dist/link.d.ts +22 -0
  84. package/dist/link.js +11 -0
  85. package/dist/switcher.d.ts +66 -0
  86. package/dist/switcher.js +53 -0
  87. package/dist/typography.d.ts +32 -24
  88. package/dist/typography.js +39 -9
  89. package/dist/ui.d.ts +25 -5
  90. package/dist/ui.js +51 -7
  91. package/package.json +19 -1
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
+ }
@@ -0,0 +1,27 @@
1
+ import type { ComponentPropsWithoutRef, ReactElement } from "react";
2
+ import { type LinkRender } from "./link.js";
3
+ /** The classes of an inline text link, for a router's own <Link> that should look like one. */
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";
10
+ type TextLinkProps = {
11
+ /** Opens in a new tab: adds `target="_blank"` and `rel="noreferrer"`. */
12
+ external?: boolean | undefined;
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"> & ({
16
+ href: string;
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;
27
+ export {};
@@ -0,0 +1,23 @@
1
+ import { cx, focusRing } from "./cx.js";
2
+ import { renderLink } from "./link.js";
3
+ /** The classes of an inline text link, for a router's own <Link> that should look like one. */
4
+ export const linkClass = cx("rounded font-medium text-violet-600 underline-offset-2 transition-colors hover:text-violet-700", focusRing);
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
+ });
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
+ }
@@ -0,0 +1,10 @@
1
+ import type { ReactElement, VideoHTMLAttributes } from "react";
2
+ /**
3
+ * A `<video>` with controls, full width and rounded; it loads metadata only, so the page does not
4
+ * pull the file until it is played. `src` is the app's own video; `captions` is a WebVTT file,
5
+ * offered as the default track. `children` is the fallback for a browser that cannot play it.
6
+ */
7
+ export declare function Video({ src, captions, className, children, ...rest }: {
8
+ src: string;
9
+ captions?: string | undefined;
10
+ } & Omit<VideoHTMLAttributes<HTMLVideoElement>, "src" | "controls">): ReactElement;
package/dist/Video.js ADDED
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from "./cx.js";
3
+ /**
4
+ * A `<video>` with controls, full width and rounded; it loads metadata only, so the page does not
5
+ * pull the file until it is played. `src` is the app's own video; `captions` is a WebVTT file,
6
+ * offered as the default track. `children` is the fallback for a browser that cannot play it.
7
+ */
8
+ export function Video({ src, captions, className, children, ...rest }) {
9
+ return (_jsxs("video", { controls: true, preload: "metadata", src: src, className: cx("w-full rounded-lg", className), ...rest, children: [captions && _jsx("track", { kind: "captions", src: captions, default: true }), children] }));
10
+ }
@@ -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,32 +1,54 @@
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
7
  export { AppShell, appShellRootClass } from "./AppShell.js";
4
8
  export { Container } from "./Container.js";
5
9
  export { PageHeader, type PageHeaderProps } from "./PageHeader.js";
6
- export { CardGrid, CenteredLayout, DetailLayout, FormLayout, FullBleedLayout, ListLayout, SidebarLayout, } from "./PageLayouts.js";
7
- export { Section, Stack, type StackGap } from "./Stack.js";
8
- export { Button, buttonClass, type ButtonVariant } from "./Button.js";
10
+ export { CardGrid, CenteredLayout, DetailLayout, FormLayout, FullBleedLayout, ListLayout, ProseLayout, SidebarLayout, } from "./PageLayouts.js";
11
+ export { Section, Stack, type StackGap, type StackWidth } from "./Stack.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";
18
+ export { BulletList } from "./BulletList.js";
11
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";
23
+ export { Code } from "./Code.js";
24
+ export { Divider } from "./Divider.js";
25
+ export { PageFade, startPageTransition } from "./PageFade.js";
12
26
  export { Dialog, type DialogProps } from "./Dialog.js";
27
+ export { Drawer, type DrawerProps } from "./Drawer.js";
13
28
  export { IconButton } from "./IconButton.js";
14
29
  export { ColourSchemeToggle } from "./ColourSchemeToggle.js";
15
- export { applyColourScheme, colourSchemeScript, readColourScheme, storeColourScheme, type ColourScheme, } from "./colourScheme.js";
30
+ export { applyColourScheme, schemeClass, type ColourSchemeClasses, colourSchemeScript, readColourScheme, storeColourScheme, type ColourScheme, } from "./colourScheme.js";
16
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";
17
34
  export { ToggleChip } from "./ToggleChip.js";
18
35
  export { FilterGroup } from "./FilterGroup.js";
19
36
  export { Checkbox } from "./Checkbox.js";
37
+ export { FileDrop } from "./FileDrop.js";
20
38
  export { CopyButton } from "./CopyButton.js";
21
39
  export { CopyLine } from "./CopyLine.js";
22
40
  export { Field } from "./Field.js";
23
41
  export { FieldDialog, type FieldDialogProps } from "./FieldDialog.js";
24
42
  export { Input } from "./Input.js";
43
+ export type { ControlSize, ControlWidth } from "./control.js";
44
+ export { InlineLinks, type InlineLinksSeparator } from "./InlineLinks.js";
25
45
  export { Kbd } from "./Kbd.js";
26
46
  export { KeyValue, KeyValueList } from "./KeyValueList.js";
27
47
  export { Select } from "./Select.js";
28
48
  export { Textarea } from "./Textarea.js";
29
- export { Body, BodyStrong, Caption, Display, Lead, ModalHeading, Overline, PageHeading, SectionHeading, SubsectionHeading, Subtitle, type TypographyColor, type TypographyProps, } from "./typography.js";
49
+ export { Video } from "./Video.js";
50
+ export { linkClass, linkStretchClass, TextLink } from "./TextLink.js";
51
+ export { Body, BodyStrong, Caption, Display, Lead, ModalHeading, Overline, PageHeading, SectionHeading, SubsectionHeading, type SubsectionHeadingSize, Subtitle, type TypographyColor, type TypographyProps, } from "./typography.js";
30
52
  export { DateField, type DateFieldProps } from "./DateField.js";
31
53
  export { DatePicker, type DatePickerProps } from "./DatePicker.js";
32
54
  export { fromISODate, toISODate } from "./isoDate.js";
package/dist/index.js CHANGED
@@ -1,31 +1,52 @@
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
7
  export { AppShell, appShellRootClass } from "./AppShell.js";
4
8
  export { Container } from "./Container.js";
5
9
  export { PageHeader } from "./PageHeader.js";
6
- export { CardGrid, CenteredLayout, DetailLayout, FormLayout, FullBleedLayout, ListLayout, SidebarLayout, } from "./PageLayouts.js";
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";
18
+ export { BulletList } from "./BulletList.js";
11
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";
23
+ export { Code } from "./Code.js";
24
+ export { Divider } from "./Divider.js";
25
+ export { PageFade, startPageTransition } from "./PageFade.js";
12
26
  export { Dialog } from "./Dialog.js";
27
+ export { Drawer } from "./Drawer.js";
13
28
  export { IconButton } from "./IconButton.js";
14
29
  export { ColourSchemeToggle } from "./ColourSchemeToggle.js";
15
- export { applyColourScheme, colourSchemeScript, readColourScheme, storeColourScheme, } from "./colourScheme.js";
30
+ export { applyColourScheme, schemeClass, colourSchemeScript, readColourScheme, storeColourScheme, } from "./colourScheme.js";
16
31
  export { NavTabs, navTabClass } from "./NavTabs.js";
32
+ export { Tabs } from "./Tabs.js";
33
+ export { SegmentedControl } from "./SegmentedControl.js";
17
34
  export { ToggleChip } from "./ToggleChip.js";
18
35
  export { FilterGroup } from "./FilterGroup.js";
19
36
  export { Checkbox } from "./Checkbox.js";
37
+ export { FileDrop } from "./FileDrop.js";
20
38
  export { CopyButton } from "./CopyButton.js";
21
39
  export { CopyLine } from "./CopyLine.js";
22
40
  export { Field } from "./Field.js";
23
41
  export { FieldDialog } from "./FieldDialog.js";
24
42
  export { Input } from "./Input.js";
43
+ export { InlineLinks } from "./InlineLinks.js";
25
44
  export { Kbd } from "./Kbd.js";
26
45
  export { KeyValue, KeyValueList } from "./KeyValueList.js";
27
46
  export { Select } from "./Select.js";
28
47
  export { Textarea } from "./Textarea.js";
48
+ export { Video } from "./Video.js";
49
+ export { linkClass, linkStretchClass, TextLink } from "./TextLink.js";
29
50
  export { Body, BodyStrong, Caption, Display, Lead, ModalHeading, Overline, PageHeading, SectionHeading, SubsectionHeading, Subtitle, } from "./typography.js";
30
51
  export { DateField } from "./DateField.js";
31
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
+ }
@@ -0,0 +1,66 @@
1
+ import { type ReactElement, type ReactNode } from "react";
2
+ import { type LinkRender } from "./link.js";
3
+ type Option = {
4
+ /** What `value` is set to when this option is current. */
5
+ value: string;
6
+ label: ReactNode;
7
+ };
8
+ /** An option that is a button: it calls `onChange(value)`. */
9
+ export type SwitchButton = Option & {
10
+ disabled?: boolean;
11
+ /** The id of the element this option controls (a tab's panel). */
12
+ controls?: string;
13
+ href?: undefined;
14
+ render?: undefined;
15
+ external?: undefined;
16
+ };
17
+ /**
18
+ * An option that is a link: `href`, or `render` to draw the app's own router `<Link>`. The router
19
+ * changes the page; `value` only marks which one is current. `external` opens a new tab.
20
+ */
21
+ export type SwitchLink = Option & {
22
+ disabled?: undefined;
23
+ controls?: undefined;
24
+ external?: boolean;
25
+ } & ({
26
+ href: string;
27
+ render?: LinkRender;
28
+ } | {
29
+ href?: string;
30
+ render: LinkRender;
31
+ });
32
+ /** One option of a Tabs or SegmentedControl: a button or a link, never both in one group. */
33
+ export type SwitchItem = SwitchButton | SwitchLink;
34
+ type Shared = {
35
+ /** The `value` of the current option. */
36
+ value: string;
37
+ /** The accessible name of the group. */
38
+ label: string;
39
+ className?: string;
40
+ };
41
+ export type SwitchProps = Shared & ({
42
+ items: SwitchButton[];
43
+ /** Called with the option's `value` when one is chosen, by click or arrow key. */
44
+ onChange: (value: string) => void;
45
+ } | {
46
+ items: SwitchLink[];
47
+ onChange?: undefined;
48
+ });
49
+ type Parts = {
50
+ /** The roles of a group of buttons; a group with links is a `<nav>` instead. */
51
+ group: "tablist" | "radiogroup";
52
+ item: "tab" | "radio";
53
+ /** The vertical arrows move too, as in a radio group. */
54
+ vertical: boolean;
55
+ root: string;
56
+ option: (active: boolean) => string;
57
+ };
58
+ /**
59
+ * The shared body of Tabs and SegmentedControl: a row of options with one current. Buttons form a
60
+ * `tablist` or `radiogroup` with a single tab stop and arrow-key movement (the arrow focuses and
61
+ * chooses); links form a `<nav>` with `aria-current="page"` on the current one.
62
+ */
63
+ export declare function Switcher({ items, value, onChange, label, className, parts, }: SwitchProps & {
64
+ parts: Parts;
65
+ }): ReactElement;
66
+ export {};
@@ -0,0 +1,53 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Fragment } from "react";
3
+ import { cx } from "./cx.js";
4
+ import { renderLink } from "./link.js";
5
+ /** The index a key moves to from `at` among `count` enabled options, wrapping; -1 for a key that does not move. */
6
+ function target(key, at, count, vertical) {
7
+ if (key === "ArrowRight" || (vertical && key === "ArrowDown"))
8
+ return (at + 1) % count;
9
+ if (key === "ArrowLeft" || (vertical && key === "ArrowUp"))
10
+ return (at - 1 + count) % count;
11
+ if (key === "Home")
12
+ return 0;
13
+ if (key === "End")
14
+ return count - 1;
15
+ return -1;
16
+ }
17
+ const isLinks = (items) => items.some((item) => item.href !== undefined || item.render !== undefined);
18
+ /**
19
+ * The shared body of Tabs and SegmentedControl: a row of options with one current. Buttons form a
20
+ * `tablist` or `radiogroup` with a single tab stop and arrow-key movement (the arrow focuses and
21
+ * chooses); links form a `<nav>` with `aria-current="page"` on the current one.
22
+ */
23
+ export function Switcher({ items, value, onChange, label, className, parts, }) {
24
+ const root = cx(parts.root, className);
25
+ if (isLinks(items)) {
26
+ return (_jsx("nav", { "aria-label": label, className: root, children: items.map((item) => (_jsx(Fragment, { children: renderLink({
27
+ href: item.href,
28
+ render: item.render,
29
+ external: item.external,
30
+ className: parts.option(item.value === value),
31
+ "aria-current": item.value === value ? "page" : undefined,
32
+ children: item.label,
33
+ }) }, item.value))) }));
34
+ }
35
+ const enabled = items.filter((item) => !item.disabled);
36
+ const stop = enabled.some((item) => item.value === value) ? value : enabled[0]?.value;
37
+ function onKeyDown(event) {
38
+ const buttons = [...event.currentTarget.querySelectorAll("button:not(:disabled)")];
39
+ const at = buttons.findIndex((button) => button === event.target);
40
+ if (at < 0)
41
+ return;
42
+ const to = target(event.key, at, buttons.length, parts.vertical);
43
+ const button = buttons[to];
44
+ if (!(button instanceof HTMLButtonElement))
45
+ return;
46
+ event.preventDefault();
47
+ button.focus();
48
+ button.click();
49
+ }
50
+ return (_jsx("div", { role: parts.group, "aria-label": label, onKeyDown: onKeyDown, className: root, children: items.map((item) => (_jsx("button", { type: "button", role: parts.item, ...(parts.item === "tab"
51
+ ? { "aria-selected": item.value === value }
52
+ : { "aria-checked": item.value === value }), ...(item.controls === undefined ? {} : { "aria-controls": item.controls }), tabIndex: item.value === stop ? 0 : -1, disabled: item.disabled === true, onClick: () => onChange?.(item.value), className: cx(parts.option(item.value === value), "disabled:opacity-50"), children: item.label }, item.value))) }));
53
+ }