@hackcove/ui-react 0.0.0-stage → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/MIGRATING.md +116 -0
- package/README.md +400 -2
- package/dist/AppShell.d.ts +97 -0
- package/dist/AppShell.js +106 -0
- package/dist/Avatar.d.ts +49 -0
- package/dist/Avatar.js +65 -0
- package/dist/AvatarStack.d.ts +29 -0
- package/dist/AvatarStack.js +26 -0
- package/dist/Badge.d.ts +14 -0
- package/dist/Badge.js +11 -0
- package/dist/Breadcrumbs.d.ts +28 -0
- package/dist/Breadcrumbs.js +30 -0
- package/dist/BulletList.d.ts +4 -0
- package/dist/BulletList.js +5 -0
- package/dist/Button.d.ts +78 -0
- package/dist/Button.js +133 -0
- package/dist/ButtonGroup.d.ts +15 -0
- package/dist/ButtonGroup.js +10 -0
- package/dist/Callout.d.ts +24 -0
- package/dist/Callout.js +28 -0
- package/dist/Card.d.ts +95 -0
- package/dist/Card.js +134 -0
- package/dist/CardBadge.d.ts +7 -0
- package/dist/CardBadge.js +12 -0
- package/dist/CardParts.d.ts +25 -0
- package/dist/CardParts.js +25 -0
- package/dist/Checkbox.d.ts +29 -0
- package/dist/Checkbox.js +36 -0
- package/dist/Chevron.d.ts +5 -0
- package/dist/Chevron.js +6 -0
- package/dist/Cluster.d.ts +51 -0
- package/dist/Cluster.js +38 -0
- package/dist/Code.d.ts +4 -0
- package/dist/Code.js +5 -0
- package/dist/CodeBlock.d.ts +27 -0
- package/dist/CodeBlock.js +12 -0
- package/dist/CodeExample.d.ts +17 -0
- package/dist/CodeExample.js +13 -0
- package/dist/CodeText.d.ts +7 -0
- package/dist/CodeText.js +21 -0
- package/dist/ColourSchemeToggle.d.ts +34 -0
- package/dist/ColourSchemeToggle.js +50 -0
- package/dist/Container.d.ts +22 -0
- package/dist/Container.js +13 -0
- package/dist/CopyButton.d.ts +17 -0
- package/dist/CopyButton.js +13 -0
- package/dist/CopyLine.d.ts +9 -0
- package/dist/CopyLine.js +7 -0
- package/dist/DateCalendar.d.ts +10 -0
- package/dist/DateCalendar.js +45 -0
- package/dist/DateField.d.ts +87 -0
- package/dist/DateField.js +100 -0
- package/dist/DateFieldCalendar.d.ts +6 -0
- package/dist/DateFieldCalendar.js +35 -0
- package/dist/Dialog.d.ts +26 -0
- package/dist/Dialog.js +18 -0
- package/dist/Disclosure.d.ts +15 -0
- package/dist/Disclosure.js +12 -0
- package/dist/Divider.d.ts +4 -0
- package/dist/Divider.js +5 -0
- package/dist/Drawer.d.ts +29 -0
- package/dist/Drawer.js +22 -0
- package/dist/EmptyState.d.ts +18 -0
- package/dist/EmptyState.js +14 -0
- package/dist/Field.d.ts +21 -0
- package/dist/Field.js +19 -0
- package/dist/FieldDialog.d.ts +28 -0
- package/dist/FieldDialog.js +34 -0
- package/dist/FileDrop.d.ts +17 -0
- package/dist/FileDrop.js +12 -0
- package/dist/FilterGroup.d.ts +7 -0
- package/dist/FilterGroup.js +6 -0
- package/dist/FormRow.d.ts +42 -0
- package/dist/FormRow.js +33 -0
- package/dist/Icon.d.ts +20 -0
- package/dist/Icon.js +13 -0
- package/dist/IconButton.d.ts +57 -0
- package/dist/IconButton.js +51 -0
- package/dist/Input.d.ts +22 -0
- package/dist/Input.js +22 -0
- package/dist/InputShell.d.ts +16 -0
- package/dist/InputShell.js +26 -0
- package/dist/Kbd.d.ts +6 -0
- package/dist/Kbd.js +5 -0
- package/dist/KeyValueList.d.ts +23 -0
- package/dist/KeyValueList.js +19 -0
- package/dist/Lightbox.d.ts +28 -0
- package/dist/Lightbox.js +44 -0
- package/dist/ListRow.d.ts +109 -0
- package/dist/ListRow.js +92 -0
- package/dist/Markdown.d.ts +9 -0
- package/dist/Markdown.js +43 -0
- package/dist/MetaList.d.ts +23 -0
- package/dist/MetaList.js +14 -0
- package/dist/OutlineRegion.d.ts +43 -0
- package/dist/OutlineRegion.js +27 -0
- package/dist/PageFade.d.ts +26 -0
- package/dist/PageFade.js +39 -0
- package/dist/PageHeader.d.ts +24 -0
- package/dist/PageHeader.js +14 -0
- package/dist/PageLayouts.d.ts +130 -0
- package/dist/PageLayouts.js +86 -0
- package/dist/Pill.d.ts +53 -0
- package/dist/Pill.js +34 -0
- package/dist/ProgressBar.d.ts +32 -0
- package/dist/ProgressBar.js +32 -0
- package/dist/RadioGroup.d.ts +48 -0
- package/dist/RadioGroup.js +39 -0
- package/dist/SearchInput.d.ts +8 -0
- package/dist/SearchInput.js +40 -0
- package/dist/SegmentedControl.d.ts +20 -0
- package/dist/SegmentedControl.js +31 -0
- package/dist/Select.d.ts +5 -0
- package/dist/Select.js +9 -0
- package/dist/SplitBar.d.ts +27 -0
- package/dist/SplitBar.js +15 -0
- package/dist/Stack.d.ts +70 -0
- package/dist/Stack.js +47 -0
- package/dist/StackedBar.d.ts +56 -0
- package/dist/StackedBar.js +35 -0
- package/dist/StatTile.d.ts +21 -0
- package/dist/StatTile.js +22 -0
- package/dist/StatusPages.d.ts +38 -0
- package/dist/StatusPages.js +57 -0
- package/dist/Steps.d.ts +17 -0
- package/dist/Steps.js +14 -0
- package/dist/StickyBar.d.ts +12 -0
- package/dist/StickyBar.js +10 -0
- package/dist/Table.d.ts +142 -0
- package/dist/Table.js +138 -0
- package/dist/Tabs.d.ts +40 -0
- package/dist/Tabs.js +49 -0
- package/dist/TextLink.d.ts +51 -0
- package/dist/TextLink.js +37 -0
- package/dist/Textarea.d.ts +5 -0
- package/dist/Textarea.js +7 -0
- package/dist/Timeline.d.ts +27 -0
- package/dist/Timeline.js +29 -0
- package/dist/Video.d.ts +13 -0
- package/dist/Video.js +9 -0
- package/dist/colourScheme.d.ts +21 -0
- package/dist/colourScheme.js +53 -0
- package/dist/control.d.ts +31 -0
- package/dist/control.js +38 -0
- package/dist/cx.d.ts +10 -0
- package/dist/cx.js +12 -0
- package/dist/highlight.d.ts +16 -0
- package/dist/highlight.js +292 -0
- package/dist/index.d.ts +70 -0
- package/dist/index.js +66 -0
- package/dist/isoDate.d.ts +5 -0
- package/dist/isoDate.js +16 -0
- package/dist/lift.harness.d.ts +2 -0
- package/dist/lift.harness.js +3 -0
- package/dist/link.d.ts +50 -0
- package/dist/link.js +25 -0
- package/dist/modal.d.ts +11 -0
- package/dist/modal.js +41 -0
- package/dist/props.d.ts +14 -0
- package/dist/props.js +1 -0
- package/dist/shownDate.d.ts +45 -0
- package/dist/shownDate.js +88 -0
- package/dist/size.d.ts +6 -0
- package/dist/size.js +1 -0
- package/dist/switcher.d.ts +88 -0
- package/dist/switcher.js +71 -0
- package/dist/typography.d.ts +123 -0
- package/dist/typography.js +120 -0
- package/dist/ui.d.ts +10 -0
- package/dist/ui.js +27 -0
- package/package.json +91 -4
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { type HTMLAttributes, type ReactElement, type ReactNode } from "react";
|
|
2
|
+
import { type ColourScheme, type ColourSchemeClasses } from "./colourScheme.js";
|
|
3
|
+
import { type LinkRender } from "./link.js";
|
|
4
|
+
import type { Passthrough, Unstyled } from "./props.js";
|
|
5
|
+
import type { Size } from "./size.js";
|
|
6
|
+
/**
|
|
7
|
+
* The class an app puts on its own `<html>`: it always reserves the scrollbar, so centred content
|
|
8
|
+
* does not shift between a short page and a long one. AppShell never touches `<html>` itself.
|
|
9
|
+
*/
|
|
10
|
+
export declare const appShellRootClass = "overflow-y-scroll";
|
|
11
|
+
/**
|
|
12
|
+
* Everything an app puts on its own `<html>` from the scheme the server knows: that scheme's class
|
|
13
|
+
* (`schemeClass`) and `appShellRootClass`, joined. `<html className={appShellRootClassFor(scheme,
|
|
14
|
+
* schemes)}>` replaces `schemeClass(scheme, schemes) + " " + appShellRootClass`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function appShellRootClassFor(scheme: ColourScheme, schemes?: ColourSchemeClasses): string;
|
|
17
|
+
/**
|
|
18
|
+
* The `brand` slot as one link: a logo and a label inside the same `<a>`, so the whole brand is the
|
|
19
|
+
* way home. The label names the link; the logo is decoration and hidden from assistive tech.
|
|
20
|
+
* `collapse` shows only the logo below `sm`, the label staying in the accessible name. With
|
|
21
|
+
* `render` it draws the app's own router `<Link>` (`render={(p) => <Link to="/" {...p} />}`).
|
|
22
|
+
*/
|
|
23
|
+
export declare function AppShellBrand({ logo, logoSize, label, collapse, href, render, ...rest }: Passthrough & {
|
|
24
|
+
/** The name beside the logo. It is the link's accessible name. */
|
|
25
|
+
label: ReactNode;
|
|
26
|
+
/** The logo slot's box: sm 20px, md 24px (the default, an `Avatar size="sm"`), lg 32px. An `<img>` or `<svg>` logo fills it. */
|
|
27
|
+
logoSize?: Size;
|
|
28
|
+
} & ({
|
|
29
|
+
/** The mark before the label, e.g. an `Avatar` or an `<img>`. */
|
|
30
|
+
logo?: ReactNode;
|
|
31
|
+
collapse?: false | undefined;
|
|
32
|
+
} | {
|
|
33
|
+
logo: ReactNode;
|
|
34
|
+
/** Below `sm` only the logo shows; the label is visually hidden and still names the link. Needs a `logo`. */
|
|
35
|
+
collapse: true;
|
|
36
|
+
}) & ({
|
|
37
|
+
/** Where the brand links, usually the app's home (`/`). */
|
|
38
|
+
href: string;
|
|
39
|
+
/** Draws the link with the app's own router `<Link>`: `(p) => <Link to="/" {...p} />`. */
|
|
40
|
+
render?: LinkRender | undefined;
|
|
41
|
+
} | {
|
|
42
|
+
/** Where the brand links, usually the app's home (`/`). */
|
|
43
|
+
href?: string | undefined;
|
|
44
|
+
/** Draws the link with the app's own router `<Link>`: `(p) => <Link to="/" {...p} />`. */
|
|
45
|
+
render: LinkRender;
|
|
46
|
+
})): ReactElement;
|
|
47
|
+
/** The breakpoint below which an `AppShellHideBelow` item is hidden. */
|
|
48
|
+
export type AppShellBreakpoint = "sm" | "md" | "lg";
|
|
49
|
+
/**
|
|
50
|
+
* An inline header item that only shows from a breakpoint up, such as the viewer's avatar and name
|
|
51
|
+
* in `controls`: below `breakpoint` it is gone (`display: none`, so assistive tech skips it too).
|
|
52
|
+
* Its children sit in a row with the header's gap.
|
|
53
|
+
*/
|
|
54
|
+
export declare function AppShellHideBelow({ breakpoint, children, ...rest }: Passthrough & {
|
|
55
|
+
/**
|
|
56
|
+
* Hidden below `sm` (640px), `md` (768px) or `lg` (1024px).
|
|
57
|
+
* @default sm
|
|
58
|
+
*/
|
|
59
|
+
breakpoint?: AppShellBreakpoint;
|
|
60
|
+
children: ReactNode;
|
|
61
|
+
}): ReactElement;
|
|
62
|
+
/**
|
|
63
|
+
* The page frame: it paints the page canvas and sets the type face, ink and font smoothing
|
|
64
|
+
* (`antialiased`), so an app writes no page-background or font rule of its own (a theme remaps
|
|
65
|
+
* the canvas and the font stacks). A sticky one-row header, an optional second `subnav` row, the main area and an
|
|
66
|
+
* optional footer. Header, main and footer each sit in a `Container`, so pages never set their own
|
|
67
|
+
* width or padding. `bleed` drops main's Container (the header keeps its own), for a layout that
|
|
68
|
+
* needs the whole width (`FullBleedLayout`). Main is a flex column. `SidebarLayout` needs no
|
|
69
|
+
* `bleed`: its rail and canvas share the header's Container.
|
|
70
|
+
*
|
|
71
|
+
* The header row is laid out by its slots, left to right: `brand`, `nav`, then `header` (anything
|
|
72
|
+
* the slots do not cover), then `controls`, pushed to the end. An app writes no wrapper or
|
|
73
|
+
* styling for any of them: an item that only fits a wide screen goes in `AppShellHideBelow`. A bar
|
|
74
|
+
* that sticks under the header as the page scrolls is a `StickyBar`.
|
|
75
|
+
*/
|
|
76
|
+
export declare function AppShell({ brand, nav, controls, header, subnav, footer, bleed, hideChromeInPrint, children, ...rest }: {
|
|
77
|
+
/** The brand: a logo and a name, in the header's brand type, with the gap between them. `AppShellBrand` makes both one link. */
|
|
78
|
+
brand?: ReactNode;
|
|
79
|
+
/** The main navigation, e.g. a `Tabs variant="pill"` of links (it is already a `<nav>`). */
|
|
80
|
+
nav?: ReactNode;
|
|
81
|
+
/** The end of the row: a picker, a theme toggle. Gap-2, vertically centred; no child shrinks, and a full-width control (a `Select` at its default) sizes to its content. */
|
|
82
|
+
controls?: ReactNode;
|
|
83
|
+
/** The escape hatch: a free node in the header row, after `brand` and `nav`. */
|
|
84
|
+
header?: ReactNode;
|
|
85
|
+
/** A second fixed-height row under the header, e.g. a `Tabs variant="pill"` of links. */
|
|
86
|
+
subnav?: ReactNode;
|
|
87
|
+
/** The page footer: small muted text in a `Container`, under a hairline. Hidden in print with `hideChromeInPrint`. */
|
|
88
|
+
footer?: ReactNode;
|
|
89
|
+
/** Main without a Container, for a full-bleed layout. */
|
|
90
|
+
bleed?: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Print only the page: the header and footer are hidden, and main's Container drops its padding
|
|
93
|
+
* (the page margin is then `@page`'s). Off by default, so a print is unchanged unless an app asks.
|
|
94
|
+
*/
|
|
95
|
+
hideChromeInPrint?: boolean;
|
|
96
|
+
children: ReactNode;
|
|
97
|
+
} & Unstyled<Omit<HTMLAttributes<HTMLDivElement>, "children">>): ReactElement;
|
package/dist/AppShell.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { ContainerFrame } from "./Container.js";
|
|
4
|
+
import { schemeClass } from "./colourScheme.js";
|
|
5
|
+
import { cx, focusRing } from "./cx.js";
|
|
6
|
+
import { renderLink } from "./link.js";
|
|
7
|
+
/**
|
|
8
|
+
* The class an app puts on its own `<html>`: it always reserves the scrollbar, so centred content
|
|
9
|
+
* does not shift between a short page and a long one. AppShell never touches `<html>` itself.
|
|
10
|
+
*/
|
|
11
|
+
export const appShellRootClass = "overflow-y-scroll";
|
|
12
|
+
/**
|
|
13
|
+
* Everything an app puts on its own `<html>` from the scheme the server knows: that scheme's class
|
|
14
|
+
* (`schemeClass`) and `appShellRootClass`, joined. `<html className={appShellRootClassFor(scheme,
|
|
15
|
+
* schemes)}>` replaces `schemeClass(scheme, schemes) + " " + appShellRootClass`.
|
|
16
|
+
*/
|
|
17
|
+
export function appShellRootClassFor(scheme, schemes) {
|
|
18
|
+
return cx(schemeClass(scheme, schemes), appShellRootClass);
|
|
19
|
+
}
|
|
20
|
+
const LOGO_BOX = { sm: "size-5", md: "size-6", lg: "size-8" };
|
|
21
|
+
/**
|
|
22
|
+
* The `brand` slot as one link: a logo and a label inside the same `<a>`, so the whole brand is the
|
|
23
|
+
* way home. The label names the link; the logo is decoration and hidden from assistive tech.
|
|
24
|
+
* `collapse` shows only the logo below `sm`, the label staying in the accessible name. With
|
|
25
|
+
* `render` it draws the app's own router `<Link>` (`render={(p) => <Link to="/" {...p} />}`).
|
|
26
|
+
*/
|
|
27
|
+
export function AppShellBrand({ logo, logoSize = "md", label, collapse = false, href, render, ...rest }) {
|
|
28
|
+
return renderLink({
|
|
29
|
+
...rest,
|
|
30
|
+
href,
|
|
31
|
+
render,
|
|
32
|
+
className: cx("flex items-center gap-2 rounded", focusRing),
|
|
33
|
+
children: (_jsxs(_Fragment, { children: [logo && (_jsx("span", { "aria-hidden": "true", className: cx("flex shrink-0 items-center justify-center [&>img]:size-full [&>svg]:size-full", LOGO_BOX[logoSize]), children: logo })), _jsx("span", { className: collapse ? "sr-only sm:not-sr-only" : undefined, children: label })] })),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
const SHOWN_FROM = {
|
|
37
|
+
sm: "hidden sm:flex",
|
|
38
|
+
md: "hidden md:flex",
|
|
39
|
+
lg: "hidden lg:flex",
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* An inline header item that only shows from a breakpoint up, such as the viewer's avatar and name
|
|
43
|
+
* in `controls`: below `breakpoint` it is gone (`display: none`, so assistive tech skips it too).
|
|
44
|
+
* Its children sit in a row with the header's gap.
|
|
45
|
+
*/
|
|
46
|
+
export function AppShellHideBelow({ breakpoint = "sm", children, ...rest }) {
|
|
47
|
+
return (_jsx("span", { ...rest, className: cx(SHOWN_FROM[breakpoint], "items-center gap-2"), children: children }));
|
|
48
|
+
}
|
|
49
|
+
// One fixed-height row that scrolls sideways and never wraps; its children keep their width.
|
|
50
|
+
const ROW = "flex items-center gap-4 overflow-x-auto whitespace-nowrap *:shrink-0";
|
|
51
|
+
// While a row has more to scroll to on a side, that edge fades out (a mask, so it follows any theme).
|
|
52
|
+
// `ScrollRow` sets `data-more-start` / `data-more-end` (and `data-more` for either); a row that fits
|
|
53
|
+
// has none and is untouched.
|
|
54
|
+
const FADE = "data-more-start:[--fade-start:2rem] data-more-end:[--fade-end:2rem] data-more:[mask-image:linear-gradient(to_right,transparent,#000_var(--fade-start,0px),#000_calc(100%_-_var(--fade-end,0px)),transparent)]";
|
|
55
|
+
/** A row in a Container, with the fade on whichever side still has content to scroll to. */
|
|
56
|
+
function ScrollRow({ className, children }) {
|
|
57
|
+
const row = useRef(null);
|
|
58
|
+
// Every render: the children may have changed, and each needs observing.
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
const el = row.current;
|
|
61
|
+
if (!el)
|
|
62
|
+
return undefined;
|
|
63
|
+
const update = () => {
|
|
64
|
+
const start = el.scrollLeft > 1;
|
|
65
|
+
const end = el.scrollWidth - el.clientWidth - el.scrollLeft > 1;
|
|
66
|
+
el.toggleAttribute("data-more-start", start);
|
|
67
|
+
el.toggleAttribute("data-more-end", end);
|
|
68
|
+
el.toggleAttribute("data-more", start || end);
|
|
69
|
+
};
|
|
70
|
+
update();
|
|
71
|
+
el.addEventListener("scroll", update, { passive: true });
|
|
72
|
+
// jsdom (a consumer's own tests) has no ResizeObserver.
|
|
73
|
+
const resize = typeof ResizeObserver === "undefined" ? undefined : new ResizeObserver(update);
|
|
74
|
+
resize?.observe(el);
|
|
75
|
+
for (const child of el.children)
|
|
76
|
+
resize?.observe(child);
|
|
77
|
+
return () => {
|
|
78
|
+
el.removeEventListener("scroll", update);
|
|
79
|
+
resize?.disconnect();
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
return (_jsx(ContainerFrame, { elementRef: (el) => void (row.current = el), className: cx(ROW, FADE, className), children: children }));
|
|
83
|
+
}
|
|
84
|
+
/** The header row's slots in order: brand, nav, the free `children`, then controls at the end. */
|
|
85
|
+
function HeaderSlots({ brand, nav, controls, children, }) {
|
|
86
|
+
return (_jsxs(_Fragment, { children: [brand && (_jsx("div", { className: "flex items-center gap-2 text-base font-bold tracking-tight text-slate-900", children: brand })), nav, children, controls && (_jsx("div", { className: "ml-auto flex items-center gap-2 *:shrink-0 [&>.w-full]:w-auto", children: controls }))] }));
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The page frame: it paints the page canvas and sets the type face, ink and font smoothing
|
|
90
|
+
* (`antialiased`), so an app writes no page-background or font rule of its own (a theme remaps
|
|
91
|
+
* the canvas and the font stacks). A sticky one-row header, an optional second `subnav` row, the main area and an
|
|
92
|
+
* optional footer. Header, main and footer each sit in a `Container`, so pages never set their own
|
|
93
|
+
* width or padding. `bleed` drops main's Container (the header keeps its own), for a layout that
|
|
94
|
+
* needs the whole width (`FullBleedLayout`). Main is a flex column. `SidebarLayout` needs no
|
|
95
|
+
* `bleed`: its rail and canvas share the header's Container.
|
|
96
|
+
*
|
|
97
|
+
* The header row is laid out by its slots, left to right: `brand`, `nav`, then `header` (anything
|
|
98
|
+
* the slots do not cover), then `controls`, pushed to the end. An app writes no wrapper or
|
|
99
|
+
* styling for any of them: an item that only fits a wide screen goes in `AppShellHideBelow`. A bar
|
|
100
|
+
* that sticks under the header as the page scrolls is a `StickyBar`.
|
|
101
|
+
*/
|
|
102
|
+
export function AppShell({ brand, nav, controls, header, subnav, footer, bleed = false, hideChromeInPrint = false, children, ...rest }) {
|
|
103
|
+
return (_jsxs("div", { ...rest, className: cx("flex min-h-dvh flex-col bg-gray-50 font-sans text-slate-900 antialiased",
|
|
104
|
+
// The sticky header's height (its rows and the bottom border), for a bar that sticks under it.
|
|
105
|
+
subnav ? "[--app-header:calc(6.5rem+1px)]" : "[--app-header:calc(3.5rem+1px)]"), children: [_jsxs("header", { className: cx("sticky top-0 z-10 border-b border-slate-200 bg-white", hideChromeInPrint && "print:hidden"), children: [_jsx(ScrollRow, { className: "h-14", children: _jsx(HeaderSlots, { brand: brand, nav: nav, controls: controls, children: header }) }), subnav && _jsx(ScrollRow, { className: "h-12", children: subnav })] }), _jsx("main", { className: "flex flex-1 flex-col", children: bleed ? (children) : (_jsx(ContainerFrame, { className: cx("py-8", hideChromeInPrint && "print:p-0"), children: children })) }), footer && (_jsx("footer", { className: cx("border-t border-slate-200", hideChromeInPrint && "print:hidden"), children: _jsx(ContainerFrame, { className: "py-6 text-sm text-slate-500", children: footer }) }))] }));
|
|
106
|
+
}
|
package/dist/Avatar.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactElement } from "react";
|
|
2
|
+
import type { Unstyled } from "./props.js";
|
|
3
|
+
declare const SIZE_CLASSES: {
|
|
4
|
+
readonly sm: "size-6 text-xs";
|
|
5
|
+
readonly md: "size-8 text-xs";
|
|
6
|
+
readonly lg: "size-10 text-base";
|
|
7
|
+
readonly xl: "size-14 text-xl";
|
|
8
|
+
readonly "2xl": "size-24 text-3xl";
|
|
9
|
+
};
|
|
10
|
+
export type AvatarSize = keyof typeof SIZE_CLASSES;
|
|
11
|
+
/** `round` for a person, `square` (rounded corners that grow with the size) for an org or a team. */
|
|
12
|
+
export type AvatarShape = "round" | "square";
|
|
13
|
+
/** The corner classes for a shape at a size; `AvatarStack`'s "+N" chip uses the same. */
|
|
14
|
+
export declare function avatarShapeClass(shape: AvatarShape, size: AvatarSize): string;
|
|
15
|
+
/** The size classes of an Avatar, shared with `AvatarStack`'s "+N" chip. */
|
|
16
|
+
export declare function avatarSizeClass(size: AvatarSize): string;
|
|
17
|
+
/** The background class for a name: the same name gets the same colour everywhere. */
|
|
18
|
+
export declare function avatarColourClass(name: string): string;
|
|
19
|
+
/** Up to two letters or digits of a name, upper-cased; `?` when it has none. */
|
|
20
|
+
export declare function initialsOf(name: string): string;
|
|
21
|
+
type AvatarProps = {
|
|
22
|
+
/** Whose avatar: its initials, its colour (a hash of the name) and its title or `alt`. */
|
|
23
|
+
name: string;
|
|
24
|
+
/** The person's photo, a local path. It replaces the initials and is announced as `name`. */
|
|
25
|
+
src?: string | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* `sm` 24px, `md` 32px, `lg` 40px, `xl` 56px or `2xl` 96px.
|
|
28
|
+
* @default md
|
|
29
|
+
*/
|
|
30
|
+
size?: AvatarSize;
|
|
31
|
+
/**
|
|
32
|
+
* `round` for a person; `square` (rounded corners) for an org or a team, so the two never read
|
|
33
|
+
* as each other.
|
|
34
|
+
* @default round
|
|
35
|
+
*/
|
|
36
|
+
shape?: AvatarShape;
|
|
37
|
+
} & Unstyled<Omit<HTMLAttributes<HTMLElement>, "children">>;
|
|
38
|
+
/**
|
|
39
|
+
* A person as a round badge of initials, or as their photo when `src` is given; `shape="square"`
|
|
40
|
+
* marks an org or a team instead. It never loads a remote image on its own: that is a request to
|
|
41
|
+
* another server for decoration, and it makes a screenshot depend on the network. `src` is the
|
|
42
|
+
* app's own local path. A row of overlapping avatars is an `AvatarStack`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function Avatar(props: AvatarProps): ReactElement;
|
|
45
|
+
/** Avatar plus the stack's ring: internal, so the ring only ever comes from `AvatarStack`. */
|
|
46
|
+
export declare function AvatarMark({ name, src, size, shape, ring, ...rest }: AvatarProps & {
|
|
47
|
+
ring?: string | false;
|
|
48
|
+
}): ReactElement;
|
|
49
|
+
export {};
|
package/dist/Avatar.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./cx.js";
|
|
3
|
+
/** The identity colours, in hash order. Each is a stock background utility; a theme remaps them. */
|
|
4
|
+
const COLOUR_CLASSES = [
|
|
5
|
+
"bg-violet-600",
|
|
6
|
+
"bg-orange-600",
|
|
7
|
+
"bg-cyan-600",
|
|
8
|
+
"bg-emerald-600",
|
|
9
|
+
"bg-sky-600",
|
|
10
|
+
"bg-pink-600",
|
|
11
|
+
"bg-amber-600",
|
|
12
|
+
];
|
|
13
|
+
const SIZE_CLASSES = {
|
|
14
|
+
sm: "size-6 text-xs",
|
|
15
|
+
md: "size-8 text-xs",
|
|
16
|
+
lg: "size-10 text-base",
|
|
17
|
+
xl: "size-14 text-xl",
|
|
18
|
+
"2xl": "size-24 text-3xl",
|
|
19
|
+
};
|
|
20
|
+
const SQUARE_CLASSES = {
|
|
21
|
+
sm: "rounded",
|
|
22
|
+
md: "rounded-md",
|
|
23
|
+
lg: "rounded-md",
|
|
24
|
+
xl: "rounded-lg",
|
|
25
|
+
"2xl": "rounded-xl",
|
|
26
|
+
};
|
|
27
|
+
/** The corner classes for a shape at a size; `AvatarStack`'s "+N" chip uses the same. */
|
|
28
|
+
export function avatarShapeClass(shape, size) {
|
|
29
|
+
return shape === "square" ? SQUARE_CLASSES[size] : "rounded-full";
|
|
30
|
+
}
|
|
31
|
+
/** The size classes of an Avatar, shared with `AvatarStack`'s "+N" chip. */
|
|
32
|
+
export function avatarSizeClass(size) {
|
|
33
|
+
return SIZE_CLASSES[size];
|
|
34
|
+
}
|
|
35
|
+
/** The background class for a name: the same name gets the same colour everywhere. */
|
|
36
|
+
export function avatarColourClass(name) {
|
|
37
|
+
let hash = 0;
|
|
38
|
+
for (const char of name)
|
|
39
|
+
hash = (hash * 31 + char.charCodeAt(0)) >>> 0;
|
|
40
|
+
return COLOUR_CLASSES[hash % COLOUR_CLASSES.length] ?? COLOUR_CLASSES[0];
|
|
41
|
+
}
|
|
42
|
+
/** Up to two letters or digits of a name, upper-cased; `?` when it has none. */
|
|
43
|
+
export function initialsOf(name) {
|
|
44
|
+
return (name
|
|
45
|
+
.replace(/[^a-z0-9]/gi, "")
|
|
46
|
+
.slice(0, 2)
|
|
47
|
+
.toUpperCase() || "?");
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A person as a round badge of initials, or as their photo when `src` is given; `shape="square"`
|
|
51
|
+
* marks an org or a team instead. It never loads a remote image on its own: that is a request to
|
|
52
|
+
* another server for decoration, and it makes a screenshot depend on the network. `src` is the
|
|
53
|
+
* app's own local path. A row of overlapping avatars is an `AvatarStack`.
|
|
54
|
+
*/
|
|
55
|
+
export function Avatar(props) {
|
|
56
|
+
return _jsx(AvatarMark, { ...props });
|
|
57
|
+
}
|
|
58
|
+
/** Avatar plus the stack's ring: internal, so the ring only ever comes from `AvatarStack`. */
|
|
59
|
+
export function AvatarMark({ name, src, size = "md", shape = "round", ring = false, ...rest }) {
|
|
60
|
+
const corners = avatarShapeClass(shape, size);
|
|
61
|
+
if (src) {
|
|
62
|
+
return (_jsx("img", { src: src, alt: name, title: name, className: cx("shrink-0 object-cover", corners, SIZE_CLASSES[size], ring), ...rest }));
|
|
63
|
+
}
|
|
64
|
+
return (_jsx("span", { "aria-hidden": "true", title: name, className: cx("inline-grid shrink-0 place-items-center font-semibold text-violet-50", corners, SIZE_CLASSES[size], avatarColourClass(name), ring), ...rest, children: initialsOf(name) }));
|
|
65
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { type AvatarShape, type AvatarSize } from "./Avatar.js";
|
|
3
|
+
import type { Passthrough } from "./props.js";
|
|
4
|
+
/** One member of an `AvatarStack`: the same `name` and optional local `src` an `Avatar` takes. */
|
|
5
|
+
export type AvatarStackPerson = {
|
|
6
|
+
name: string;
|
|
7
|
+
src?: string | undefined;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Members as a row of overlapping avatars, each in a ring in the page colour that separates it from
|
|
11
|
+
* the next. With `max`, the first `max` show and a "+N" chip of the same size and shape stands for
|
|
12
|
+
* the rest. The row is one image to assistive tech, named by the people in it.
|
|
13
|
+
*/
|
|
14
|
+
export declare function AvatarStack({ people, max, size, shape, ...rest }: {
|
|
15
|
+
/** Who is in the stack, in order. Give each person's full name, as for `Avatar`. */
|
|
16
|
+
people: readonly AvatarStackPerson[];
|
|
17
|
+
/** How many avatars show before a "+N" chip takes the rest. Without it every person shows. */
|
|
18
|
+
max?: number | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* The size of every avatar and of the chip, as `Avatar`'s `size`.
|
|
21
|
+
* @default md
|
|
22
|
+
*/
|
|
23
|
+
size?: AvatarSize;
|
|
24
|
+
/**
|
|
25
|
+
* `square` for a stack of orgs or teams, as `Avatar`'s `shape`.
|
|
26
|
+
* @default round
|
|
27
|
+
*/
|
|
28
|
+
shape?: AvatarShape;
|
|
29
|
+
} & Passthrough): ReactElement;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { AvatarMark, avatarShapeClass, avatarSizeClass, } from "./Avatar.js";
|
|
3
|
+
import { cx } from "./cx.js";
|
|
4
|
+
// Each avatar after the first overlaps the one before it by about a quarter of its width.
|
|
5
|
+
const OVERLAP = {
|
|
6
|
+
sm: "-ml-1.5",
|
|
7
|
+
md: "-ml-2",
|
|
8
|
+
lg: "-ml-2.5",
|
|
9
|
+
xl: "-ml-3.5",
|
|
10
|
+
"2xl": "-ml-6",
|
|
11
|
+
};
|
|
12
|
+
// The ring in the page colour that keeps two overlapping avatars apart.
|
|
13
|
+
const RING = "border-2 border-slate-50";
|
|
14
|
+
/**
|
|
15
|
+
* Members as a row of overlapping avatars, each in a ring in the page colour that separates it from
|
|
16
|
+
* the next. With `max`, the first `max` show and a "+N" chip of the same size and shape stands for
|
|
17
|
+
* the rest. The row is one image to assistive tech, named by the people in it.
|
|
18
|
+
*/
|
|
19
|
+
export function AvatarStack({ people, max, size = "md", shape = "round", ...rest }) {
|
|
20
|
+
const shown = max === undefined ? people : people.slice(0, Math.max(max, 0));
|
|
21
|
+
const hidden = people.slice(shown.length);
|
|
22
|
+
const overlap = OVERLAP[size];
|
|
23
|
+
const names = shown.map((person) => person.name);
|
|
24
|
+
const label = hidden.length > 0 ? `${names.join(", ")} and ${hidden.length} more` : names.join(", ");
|
|
25
|
+
return (_jsxs("span", { role: "img", "aria-label": label, className: "inline-flex items-center", ...rest, children: [shown.map((person, index) => (_jsx(AvatarMark, { name: person.name, src: person.src, size: size, shape: shape, ring: cx(RING, index > 0 && overlap) }, `${index}:${person.name}`))), hidden.length > 0 && (_jsxs("span", { "aria-hidden": "true", title: hidden.map((person) => person.name).join(", "), className: cx("inline-grid shrink-0 place-items-center bg-slate-100 font-semibold text-slate-700 tabular-nums", RING, avatarSizeClass(size), avatarShapeClass(shape, size), shown.length > 0 && overlap), children: ["+", hidden.length] }))] }));
|
|
26
|
+
}
|
package/dist/Badge.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "react";
|
|
2
|
+
import type { Unstyled } from "./props.js";
|
|
3
|
+
/**
|
|
4
|
+
* A small red count, 10px, for sitting on an icon. `corner` pins it to the top-right corner of its
|
|
5
|
+
* `relative` parent (wrap the icon button and the badge in a `relative inline-flex` element); a
|
|
6
|
+
* count above 99 reads `99+`. Name the control it sits on (`aria-label` on the button) for a
|
|
7
|
+
* screen reader. An `IconButton` takes `badge` itself and does all of this.
|
|
8
|
+
*/
|
|
9
|
+
export declare function Badge({ count, corner, ...rest }: {
|
|
10
|
+
/** The number shown; above 99 it reads `99+`. */
|
|
11
|
+
count: number;
|
|
12
|
+
/** Pin it to the top-right corner of its `relative` parent, over the icon. */
|
|
13
|
+
corner?: boolean;
|
|
14
|
+
} & Unstyled<Omit<HTMLAttributes<HTMLSpanElement>, "children">>): import("react").JSX.Element;
|
package/dist/Badge.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./cx.js";
|
|
3
|
+
/**
|
|
4
|
+
* A small red count, 10px, for sitting on an icon. `corner` pins it to the top-right corner of its
|
|
5
|
+
* `relative` parent (wrap the icon button and the badge in a `relative inline-flex` element); a
|
|
6
|
+
* count above 99 reads `99+`. Name the control it sits on (`aria-label` on the button) for a
|
|
7
|
+
* screen reader. An `IconButton` takes `badge` itself and does all of this.
|
|
8
|
+
*/
|
|
9
|
+
export function Badge({ count, corner = false, ...rest }) {
|
|
10
|
+
return (_jsx("span", { ...rest, className: cx("inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-red-600 px-1 text-[10px] font-semibold leading-none text-red-50", corner && "absolute -right-1.5 -top-1.5"), children: count > 99 ? "99+" : count }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode } from "react";
|
|
2
|
+
import { type LinkRender } from "./link.js";
|
|
3
|
+
import type { Passthrough } from "./props.js";
|
|
4
|
+
/** The classes of a crumb that links somewhere, for a router's own <Link>. The current page is plain text, not a link. */
|
|
5
|
+
export declare const crumbLinkClass: string;
|
|
6
|
+
/**
|
|
7
|
+
* A crumb that links: a plain `<a href>`, or with `render` the app's own router `<Link>`
|
|
8
|
+
* (`render: (p) => <Link to="/quotes" {...p} />`; `href` is then not used). `external` opens in a
|
|
9
|
+
* new tab. The last crumb is the current page and is never a link.
|
|
10
|
+
*/
|
|
11
|
+
export type Crumb = {
|
|
12
|
+
label: ReactNode;
|
|
13
|
+
external?: boolean;
|
|
14
|
+
} & ({
|
|
15
|
+
href: string;
|
|
16
|
+
render?: LinkRender;
|
|
17
|
+
} | {
|
|
18
|
+
href?: string;
|
|
19
|
+
render: LinkRender;
|
|
20
|
+
});
|
|
21
|
+
/**
|
|
22
|
+
* `org / project / build / snapshot`: each crumb truncates, and the last is the current page. A
|
|
23
|
+
* crumb is a node, or a `Crumb` object when it links.
|
|
24
|
+
*/
|
|
25
|
+
export declare function Breadcrumbs({ crumbs, ...rest }: {
|
|
26
|
+
/** The trail, first to current: a node, or a `Crumb` (`{ label, href | render, external? }`) for one that links. The last is the current page and never a link. */
|
|
27
|
+
crumbs: Array<Crumb | ReactNode>;
|
|
28
|
+
} & Passthrough): ReactElement;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { isValidElement } from "react";
|
|
3
|
+
import { cx, focusRing } from "./cx.js";
|
|
4
|
+
import { renderLink } from "./link.js";
|
|
5
|
+
/** The classes of a crumb that links somewhere, for a router's own <Link>. The current page is plain text, not a link. */
|
|
6
|
+
export const crumbLinkClass = cx("rounded text-slate-500 hover:underline", focusRing);
|
|
7
|
+
function isCrumb(crumb) {
|
|
8
|
+
return typeof crumb === "object" && crumb !== null && !isValidElement(crumb) && "label" in crumb;
|
|
9
|
+
}
|
|
10
|
+
function CrumbContent({ crumb, current, }) {
|
|
11
|
+
if (!isCrumb(crumb))
|
|
12
|
+
return crumb;
|
|
13
|
+
if (current)
|
|
14
|
+
return crumb.label;
|
|
15
|
+
return renderLink({
|
|
16
|
+
href: crumb.href,
|
|
17
|
+
render: crumb.render,
|
|
18
|
+
external: crumb.external,
|
|
19
|
+
className: crumbLinkClass,
|
|
20
|
+
children: crumb.label,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* `org / project / build / snapshot`: each crumb truncates, and the last is the current page. A
|
|
25
|
+
* crumb is a node, or a `Crumb` object when it links.
|
|
26
|
+
*/
|
|
27
|
+
export function Breadcrumbs({ crumbs, ...rest }) {
|
|
28
|
+
const last = crumbs.length - 1;
|
|
29
|
+
return (_jsx("nav", { ...rest, "aria-label": "Breadcrumb", className: "min-w-0", children: _jsx("ol", { className: "flex min-w-0 items-center gap-2 text-sm", children: crumbs.map((crumb, index) => (_jsxs("li", { "aria-current": index === last ? "page" : undefined, className: "flex min-w-0 items-center gap-2", children: [index > 0 && (_jsx("span", { "aria-hidden": "true", className: "text-slate-400", children: "/" })), _jsx("span", { className: cx("min-w-0 truncate", index === last ? "font-medium text-slate-900" : "text-slate-500"), children: _jsx(CrumbContent, { crumb: crumb, current: index === last }) })] }, index))) }) }));
|
|
30
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactElement } from "react";
|
|
2
|
+
import type { Unstyled } from "./props.js";
|
|
3
|
+
/** A bulleted `<ul>`: discs, indented, a small gap between items. Its children are `<li>`s (e.g. `Text as="li"`). */
|
|
4
|
+
export declare function BulletList(props: Unstyled<HTMLAttributes<HTMLUListElement>>): ReactElement;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/** A bulleted `<ul>`: discs, indented, a small gap between items. Its children are `<li>`s (e.g. `Text as="li"`). */
|
|
3
|
+
export function BulletList(props) {
|
|
4
|
+
return _jsx("ul", { ...props, className: "list-disc space-y-1 pl-5" });
|
|
5
|
+
}
|
package/dist/Button.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactElement } from "react";
|
|
2
|
+
import { type LinkRender } from "./link.js";
|
|
3
|
+
import type { Unstyled } from "./props.js";
|
|
4
|
+
import type { Size } from "./size.js";
|
|
5
|
+
export type ButtonVariant = "primary" | "outline" | "ghost" | "link" | "dashed";
|
|
6
|
+
export type ButtonSize = Size;
|
|
7
|
+
export type ButtonTone = "default" | "positive" | "danger";
|
|
8
|
+
/** Size, tone and variant: the look of a button, apart from its state. */
|
|
9
|
+
export type ButtonLook = {
|
|
10
|
+
/**
|
|
11
|
+
* `primary` is filled, for the one main action; `outline` and `ghost` are quieter; `link` is
|
|
12
|
+
* text with an underline on hover; `dashed` is an "add another" row that fills its container.
|
|
13
|
+
* @default primary
|
|
14
|
+
*/
|
|
15
|
+
variant?: ButtonVariant | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* `sm` is compact, for table rows and toolbars; `lg` is larger, for a hero call to action.
|
|
18
|
+
* @default md
|
|
19
|
+
*/
|
|
20
|
+
size?: ButtonSize | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* `positive` is green, for accept and approve; `danger` is red, for delete and clear. Not for
|
|
23
|
+
* `dashed`, which has the one look.
|
|
24
|
+
* @default default
|
|
25
|
+
*/
|
|
26
|
+
tone?: ButtonTone | undefined;
|
|
27
|
+
};
|
|
28
|
+
/** The classes of an idle Button: what `href` draws, and what the tests compare against. */
|
|
29
|
+
export declare function buttonClass(variant?: ButtonVariant, { size, tone }?: Omit<ButtonLook, "variant">): string;
|
|
30
|
+
type ButtonAsButton = ButtonLook & {
|
|
31
|
+
/** Why the button cannot be pressed: disables it, sets it as the `title`, and greys it flat. */
|
|
32
|
+
disabledReason?: string | undefined;
|
|
33
|
+
/** The work is in flight: disabled, `aria-busy`, keeps its colour. Wins over `disabledReason`. */
|
|
34
|
+
busy?: boolean | undefined;
|
|
35
|
+
/** The label while `busy`, in place of the children. */
|
|
36
|
+
busyLabel?: string | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* A toggle: sets `aria-pressed`. On `outline` and `ghost`, `false` draws the neutral off look
|
|
39
|
+
* and `true` the tone's primary fill, so a row of filter chips shows the selected one plainly.
|
|
40
|
+
* Left out, the button is not a toggle, has no `aria-pressed` and keeps its variant's look.
|
|
41
|
+
*/
|
|
42
|
+
pressed?: boolean | undefined;
|
|
43
|
+
href?: undefined;
|
|
44
|
+
render?: undefined;
|
|
45
|
+
external?: undefined;
|
|
46
|
+
} & Unstyled<ComponentPropsWithoutRef<"button">>;
|
|
47
|
+
type ButtonAsLink = ButtonLook & {
|
|
48
|
+
/** Opens in a new tab: adds `target="_blank"` and `rel="noreferrer"`. */
|
|
49
|
+
external?: boolean | undefined;
|
|
50
|
+
disabledReason?: undefined;
|
|
51
|
+
busy?: undefined;
|
|
52
|
+
busyLabel?: undefined;
|
|
53
|
+
pressed?: undefined;
|
|
54
|
+
} & Unstyled<Omit<ComponentPropsWithoutRef<"a">, "href">> & ({
|
|
55
|
+
/** Makes the button a link to this URL. */
|
|
56
|
+
href: string;
|
|
57
|
+
/** Draws the link with the app's own router `<Link>`: `(p) => <Link to="/quotes" {...p} />`. */
|
|
58
|
+
render?: LinkRender | undefined;
|
|
59
|
+
} | {
|
|
60
|
+
/** Makes the button a link to this URL. */
|
|
61
|
+
href?: string | undefined;
|
|
62
|
+
/** Draws the link with the app's own router `<Link>`: `(p) => <Link to="/quotes" {...p} />`. */
|
|
63
|
+
render: LinkRender;
|
|
64
|
+
});
|
|
65
|
+
type ButtonProps = ButtonAsButton | ButtonAsLink;
|
|
66
|
+
/**
|
|
67
|
+
* A button in the primary, outline, ghost, link or dashed style (the link has no box: text with an
|
|
68
|
+
* underline on hover, for an in-line action; `dashed` is the full-width "add" row, a dashed
|
|
69
|
+
* outline with muted text that fills its container, with an optional leading icon in the
|
|
70
|
+
* children). `size="sm"` is compact and `size="lg"` larger; `tone="positive"` is green and `tone="danger"` is red;
|
|
71
|
+
* `pressed` makes it a toggle: neutral when off, the tone's fill when on.
|
|
72
|
+
* Defaults to `type="button"`. `disabledReason` and `busy` are the two disabled states that say
|
|
73
|
+
* more than a plain `disabled`, which keeps its half-opacity look. `href` makes it a link instead,
|
|
74
|
+
* with the same look (a plain `<a>`, or with `render` the app's own router `<Link>`:
|
|
75
|
+
* `render={(p) => <Link to="/quotes" {...p} />}`; `external` opens a new tab).
|
|
76
|
+
*/
|
|
77
|
+
export declare function Button(props: ButtonProps): ReactElement;
|
|
78
|
+
export {};
|