@hackcove/ui-react 0.0.0-stage → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/LICENSE +21 -0
  2. package/MIGRATING.md +116 -0
  3. package/README.md +400 -2
  4. package/dist/AppShell.d.ts +97 -0
  5. package/dist/AppShell.js +106 -0
  6. package/dist/Avatar.d.ts +49 -0
  7. package/dist/Avatar.js +65 -0
  8. package/dist/AvatarStack.d.ts +29 -0
  9. package/dist/AvatarStack.js +26 -0
  10. package/dist/Badge.d.ts +14 -0
  11. package/dist/Badge.js +11 -0
  12. package/dist/Breadcrumbs.d.ts +28 -0
  13. package/dist/Breadcrumbs.js +30 -0
  14. package/dist/BulletList.d.ts +4 -0
  15. package/dist/BulletList.js +5 -0
  16. package/dist/Button.d.ts +78 -0
  17. package/dist/Button.js +133 -0
  18. package/dist/ButtonGroup.d.ts +15 -0
  19. package/dist/ButtonGroup.js +10 -0
  20. package/dist/Callout.d.ts +24 -0
  21. package/dist/Callout.js +28 -0
  22. package/dist/Card.d.ts +95 -0
  23. package/dist/Card.js +134 -0
  24. package/dist/CardBadge.d.ts +7 -0
  25. package/dist/CardBadge.js +12 -0
  26. package/dist/CardParts.d.ts +25 -0
  27. package/dist/CardParts.js +25 -0
  28. package/dist/Checkbox.d.ts +29 -0
  29. package/dist/Checkbox.js +36 -0
  30. package/dist/Chevron.d.ts +5 -0
  31. package/dist/Chevron.js +6 -0
  32. package/dist/Cluster.d.ts +51 -0
  33. package/dist/Cluster.js +38 -0
  34. package/dist/Code.d.ts +4 -0
  35. package/dist/Code.js +5 -0
  36. package/dist/CodeBlock.d.ts +27 -0
  37. package/dist/CodeBlock.js +12 -0
  38. package/dist/CodeExample.d.ts +17 -0
  39. package/dist/CodeExample.js +13 -0
  40. package/dist/CodeText.d.ts +7 -0
  41. package/dist/CodeText.js +21 -0
  42. package/dist/ColourSchemeToggle.d.ts +34 -0
  43. package/dist/ColourSchemeToggle.js +50 -0
  44. package/dist/Container.d.ts +22 -0
  45. package/dist/Container.js +13 -0
  46. package/dist/CopyButton.d.ts +17 -0
  47. package/dist/CopyButton.js +13 -0
  48. package/dist/CopyLine.d.ts +9 -0
  49. package/dist/CopyLine.js +7 -0
  50. package/dist/DateCalendar.d.ts +10 -0
  51. package/dist/DateCalendar.js +45 -0
  52. package/dist/DateField.d.ts +87 -0
  53. package/dist/DateField.js +100 -0
  54. package/dist/DateFieldCalendar.d.ts +6 -0
  55. package/dist/DateFieldCalendar.js +35 -0
  56. package/dist/Dialog.d.ts +26 -0
  57. package/dist/Dialog.js +18 -0
  58. package/dist/Disclosure.d.ts +15 -0
  59. package/dist/Disclosure.js +12 -0
  60. package/dist/Divider.d.ts +4 -0
  61. package/dist/Divider.js +5 -0
  62. package/dist/Drawer.d.ts +29 -0
  63. package/dist/Drawer.js +22 -0
  64. package/dist/EmptyState.d.ts +18 -0
  65. package/dist/EmptyState.js +14 -0
  66. package/dist/Field.d.ts +21 -0
  67. package/dist/Field.js +19 -0
  68. package/dist/FieldDialog.d.ts +28 -0
  69. package/dist/FieldDialog.js +34 -0
  70. package/dist/FileDrop.d.ts +17 -0
  71. package/dist/FileDrop.js +12 -0
  72. package/dist/FilterGroup.d.ts +7 -0
  73. package/dist/FilterGroup.js +6 -0
  74. package/dist/FormRow.d.ts +42 -0
  75. package/dist/FormRow.js +33 -0
  76. package/dist/Icon.d.ts +20 -0
  77. package/dist/Icon.js +13 -0
  78. package/dist/IconButton.d.ts +57 -0
  79. package/dist/IconButton.js +51 -0
  80. package/dist/Input.d.ts +22 -0
  81. package/dist/Input.js +22 -0
  82. package/dist/InputShell.d.ts +16 -0
  83. package/dist/InputShell.js +26 -0
  84. package/dist/Kbd.d.ts +6 -0
  85. package/dist/Kbd.js +5 -0
  86. package/dist/KeyValueList.d.ts +23 -0
  87. package/dist/KeyValueList.js +19 -0
  88. package/dist/Lightbox.d.ts +28 -0
  89. package/dist/Lightbox.js +44 -0
  90. package/dist/ListRow.d.ts +109 -0
  91. package/dist/ListRow.js +92 -0
  92. package/dist/Markdown.d.ts +9 -0
  93. package/dist/Markdown.js +43 -0
  94. package/dist/MetaList.d.ts +23 -0
  95. package/dist/MetaList.js +14 -0
  96. package/dist/OutlineRegion.d.ts +43 -0
  97. package/dist/OutlineRegion.js +27 -0
  98. package/dist/PageFade.d.ts +26 -0
  99. package/dist/PageFade.js +39 -0
  100. package/dist/PageHeader.d.ts +24 -0
  101. package/dist/PageHeader.js +14 -0
  102. package/dist/PageLayouts.d.ts +130 -0
  103. package/dist/PageLayouts.js +86 -0
  104. package/dist/Pill.d.ts +53 -0
  105. package/dist/Pill.js +34 -0
  106. package/dist/ProgressBar.d.ts +32 -0
  107. package/dist/ProgressBar.js +32 -0
  108. package/dist/RadioGroup.d.ts +48 -0
  109. package/dist/RadioGroup.js +39 -0
  110. package/dist/SearchInput.d.ts +8 -0
  111. package/dist/SearchInput.js +40 -0
  112. package/dist/SegmentedControl.d.ts +20 -0
  113. package/dist/SegmentedControl.js +31 -0
  114. package/dist/Select.d.ts +5 -0
  115. package/dist/Select.js +9 -0
  116. package/dist/SplitBar.d.ts +27 -0
  117. package/dist/SplitBar.js +15 -0
  118. package/dist/Stack.d.ts +70 -0
  119. package/dist/Stack.js +47 -0
  120. package/dist/StackedBar.d.ts +56 -0
  121. package/dist/StackedBar.js +35 -0
  122. package/dist/StatTile.d.ts +21 -0
  123. package/dist/StatTile.js +22 -0
  124. package/dist/StatusPages.d.ts +38 -0
  125. package/dist/StatusPages.js +57 -0
  126. package/dist/Steps.d.ts +17 -0
  127. package/dist/Steps.js +14 -0
  128. package/dist/StickyBar.d.ts +12 -0
  129. package/dist/StickyBar.js +10 -0
  130. package/dist/Table.d.ts +142 -0
  131. package/dist/Table.js +138 -0
  132. package/dist/Tabs.d.ts +40 -0
  133. package/dist/Tabs.js +49 -0
  134. package/dist/TextLink.d.ts +51 -0
  135. package/dist/TextLink.js +37 -0
  136. package/dist/Textarea.d.ts +12 -0
  137. package/dist/Textarea.js +8 -0
  138. package/dist/Timeline.d.ts +27 -0
  139. package/dist/Timeline.js +29 -0
  140. package/dist/Video.d.ts +13 -0
  141. package/dist/Video.js +9 -0
  142. package/dist/colourScheme.d.ts +21 -0
  143. package/dist/colourScheme.js +53 -0
  144. package/dist/control.d.ts +31 -0
  145. package/dist/control.js +38 -0
  146. package/dist/cx.d.ts +12 -0
  147. package/dist/cx.js +14 -0
  148. package/dist/highlight.d.ts +16 -0
  149. package/dist/highlight.js +292 -0
  150. package/dist/index.d.ts +70 -0
  151. package/dist/index.js +66 -0
  152. package/dist/isoDate.d.ts +5 -0
  153. package/dist/isoDate.js +16 -0
  154. package/dist/lift.harness.d.ts +2 -0
  155. package/dist/lift.harness.js +3 -0
  156. package/dist/link.d.ts +50 -0
  157. package/dist/link.js +25 -0
  158. package/dist/modal.d.ts +11 -0
  159. package/dist/modal.js +41 -0
  160. package/dist/props.d.ts +14 -0
  161. package/dist/props.js +1 -0
  162. package/dist/shownDate.d.ts +45 -0
  163. package/dist/shownDate.js +88 -0
  164. package/dist/size.d.ts +6 -0
  165. package/dist/size.js +1 -0
  166. package/dist/switcher.d.ts +88 -0
  167. package/dist/switcher.js +71 -0
  168. package/dist/typography.d.ts +123 -0
  169. package/dist/typography.js +120 -0
  170. package/dist/ui.d.ts +10 -0
  171. package/dist/ui.js +27 -0
  172. package/package.json +91 -4
@@ -0,0 +1,43 @@
1
+ import type { HTMLAttributes, ImgHTMLAttributes, ReactElement, ReactNode } from "react";
2
+ import type { Unstyled } from "./props.js";
3
+ /** A box in a picture's own units (an image's pixels): its top-left corner and its size. */
4
+ export type RegionRect = {
5
+ x: number;
6
+ y: number;
7
+ width: number;
8
+ height: number;
9
+ };
10
+ /**
11
+ * An accent outline over one area of a picture, such as a screenshot's ignored or flagged region.
12
+ * `rect` is in the picture's own units and `within` is the picture's full size in the same units;
13
+ * the box is placed in percent of them, so it sits right at any scale. Put it in a `RegionFrame`,
14
+ * over the picture. `label` is a small tag in its top-left
15
+ * corner; `hatched` stripes the inside, for an area that is left out. It takes no pointer events,
16
+ * so it never blocks the picture or a layer drawn over it.
17
+ */
18
+ export declare function OutlineRegion({ rect, within, label, hatched, ...rest }: {
19
+ /** The area, in the picture's own units. */
20
+ rect: RegionRect;
21
+ /** The picture's full size, in the same units as `rect`. */
22
+ within: {
23
+ width: number;
24
+ height: number;
25
+ };
26
+ /** A short tag in the top-left corner: where the area came from, or what it is. */
27
+ label?: ReactNode;
28
+ /** Stripes the inside, for an area left out (ignored, masked). */
29
+ hatched?: boolean | undefined;
30
+ } & Unstyled<Omit<HTMLAttributes<HTMLDivElement>, "children">>): ReactElement;
31
+ /**
32
+ * A picture drawn full width with a hairline edge, and `children` laid over it: the
33
+ * `OutlineRegion`s that mark its areas. Every other prop goes to the `<img>` (`onLoad`, `loading`,
34
+ * `data-*`), so the app can read the picture's own size for `within`.
35
+ */
36
+ export declare function RegionFrame({ src, alt, children, ...rest }: {
37
+ /** The picture. */
38
+ src: string;
39
+ /** What the picture shows; `""` for one that only decorates. */
40
+ alt: string;
41
+ /** The `OutlineRegion`s over the picture. */
42
+ children?: ReactNode;
43
+ } & Unstyled<Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "alt" | "children">>): ReactElement;
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /** Thin accent stripes, drawn in the box's own colour, for an area that is left out. */
3
+ const HATCH = "absolute inset-0 opacity-40 bg-[repeating-linear-gradient(45deg,currentColor_0_1px,transparent_1px_6px)]";
4
+ /**
5
+ * An accent outline over one area of a picture, such as a screenshot's ignored or flagged region.
6
+ * `rect` is in the picture's own units and `within` is the picture's full size in the same units;
7
+ * the box is placed in percent of them, so it sits right at any scale. Put it in a `RegionFrame`,
8
+ * over the picture. `label` is a small tag in its top-left
9
+ * corner; `hatched` stripes the inside, for an area that is left out. It takes no pointer events,
10
+ * so it never blocks the picture or a layer drawn over it.
11
+ */
12
+ export function OutlineRegion({ rect, within, label, hatched = false, ...rest }) {
13
+ return (_jsxs("div", { ...rest, className: "pointer-events-none absolute border-2 border-violet-600 text-violet-600", style: {
14
+ left: `${(rect.x / within.width) * 100}%`,
15
+ top: `${(rect.y / within.height) * 100}%`,
16
+ width: `${(rect.width / within.width) * 100}%`,
17
+ height: `${(rect.height / within.height) * 100}%`,
18
+ }, children: [hatched && _jsx("span", { "aria-hidden": "true", className: HATCH }), label != null && (_jsx("span", { className: "absolute top-0 left-0 bg-violet-600 px-1 text-xs leading-tight text-violet-50", children: label }))] }));
19
+ }
20
+ /**
21
+ * A picture drawn full width with a hairline edge, and `children` laid over it: the
22
+ * `OutlineRegion`s that mark its areas. Every other prop goes to the `<img>` (`onLoad`, `loading`,
23
+ * `data-*`), so the app can read the picture's own size for `within`.
24
+ */
25
+ export function RegionFrame({ src, alt, children, ...rest }) {
26
+ return (_jsxs("div", { className: "relative border border-slate-200", children: [_jsx("img", { ...rest, src: src, alt: alt, className: "block w-full" }), children] }));
27
+ }
@@ -0,0 +1,26 @@
1
+ import type { HTMLAttributes, ReactElement, ReactNode } from "react";
2
+ import type { Unstyled } from "./props.js";
3
+ /**
4
+ * The page's content area, named for the View Transitions API so a route change cross-fades it
5
+ * while the frame around it (header, footer) stays put. Wrap what changes between pages, once per
6
+ * page: two on one page need a different `name` each, or the browser skips the transition.
7
+ * The fade runs only when the navigation is wrapped in a view transition: `startPageTransition`,
8
+ * or a router's own option (TanStack Router `viewTransition`, React Router's `viewTransition` prop,
9
+ * Astro's `ClientRouter`). Otherwise, or where the browser has no View Transitions, it renders
10
+ * and behaves as a plain `<div>`. A `view-transition-name` makes the div a stacking context, so a
11
+ * dropdown inside it cannot rise above a sticky header outside it: portal overlays to the body. Under `prefers-reduced-motion: reduce` this content swaps with no fade; a router's own
12
+ * transition of the rest of the page is the router's to quiet.
13
+ */
14
+ export declare function PageFade({ name, children, ...rest }: {
15
+ /** The view-transition name, a CSS identifier (letters, digits, `-`, `_`). */
16
+ name?: string;
17
+ children: ReactNode;
18
+ } & Unstyled<HTMLAttributes<HTMLDivElement>>): ReactElement;
19
+ /**
20
+ * Run `update` inside a view transition, so a `PageFade` cross-fades. `update` is a synchronous
21
+ * state change, flushed at once as the browser needs the new DOM; for a router's navigation, which
22
+ * is async, use the router's own view-transition option instead. Where the browser has
23
+ * no View Transitions, or the user prefers reduced motion, `update` just runs. Call it from an event
24
+ * handler, not during render.
25
+ */
26
+ export declare function startPageTransition(update: () => void): void;
@@ -0,0 +1,39 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { flushSync } from "react-dom";
3
+ const DEFAULT_NAME = "hc-page";
4
+ // The one place the View Transitions pseudo-elements can be styled: a stylesheet, as Tailwind
5
+ // classes cannot reach `::view-transition-*`. The browser's own cross-fade does the fading; this
6
+ // names the element and, under reduced motion, switches the animations off so the content swaps.
7
+ // No colour in it. Where the API is missing the rules are never matched.
8
+ const css = (name) => `[data-page-fade="${name}"]{view-transition-name:${name}}
9
+ @media (prefers-reduced-motion:reduce){::view-transition-group(${name}),::view-transition-old(${name}),::view-transition-new(${name}){animation:none}}`;
10
+ /**
11
+ * The page's content area, named for the View Transitions API so a route change cross-fades it
12
+ * while the frame around it (header, footer) stays put. Wrap what changes between pages, once per
13
+ * page: two on one page need a different `name` each, or the browser skips the transition.
14
+ * The fade runs only when the navigation is wrapped in a view transition: `startPageTransition`,
15
+ * or a router's own option (TanStack Router `viewTransition`, React Router's `viewTransition` prop,
16
+ * Astro's `ClientRouter`). Otherwise, or where the browser has no View Transitions, it renders
17
+ * and behaves as a plain `<div>`. A `view-transition-name` makes the div a stacking context, so a
18
+ * dropdown inside it cannot rise above a sticky header outside it: portal overlays to the body. Under `prefers-reduced-motion: reduce` this content swaps with no fade; a router's own
19
+ * transition of the rest of the page is the router's to quiet.
20
+ */
21
+ export function PageFade({ name = DEFAULT_NAME, children, ...rest }) {
22
+ return (_jsxs(_Fragment, { children: [_jsx("style", { children: css(name) }), _jsx("div", { ...rest, "data-page-fade": name, children: children })] }));
23
+ }
24
+ /**
25
+ * Run `update` inside a view transition, so a `PageFade` cross-fades. `update` is a synchronous
26
+ * state change, flushed at once as the browser needs the new DOM; for a router's navigation, which
27
+ * is async, use the router's own view-transition option instead. Where the browser has
28
+ * no View Transitions, or the user prefers reduced motion, `update` just runs. Call it from an event
29
+ * handler, not during render.
30
+ */
31
+ export function startPageTransition(update) {
32
+ const reduce = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
33
+ if (reduce || typeof document.startViewTransition !== "function") {
34
+ update();
35
+ return;
36
+ }
37
+ // A skipped transition (a second one starting, a hidden tab) rejects `ready`; the update still ran.
38
+ document.startViewTransition(() => flushSync(update)).ready.catch(() => { });
39
+ }
@@ -0,0 +1,24 @@
1
+ import type { ReactElement, ReactNode } from "react";
2
+ import type { Passthrough } from "./props.js";
3
+ export type PageHeaderProps = {
4
+ /** A `Breadcrumbs` above the title. */
5
+ breadcrumbs?: ReactNode;
6
+ /** The page's `h1`. */
7
+ title: ReactNode;
8
+ /** A muted line under the title. */
9
+ subtitle?: ReactNode;
10
+ /** Right of the title on `sm` and up, below it on a phone. */
11
+ actions?: ReactNode;
12
+ /** A `Tabs variant="pill"` (or similar) under the title. */
13
+ tabs?: ReactNode;
14
+ };
15
+ /** The header block without its bottom margin; PageHeader adds the margin (a `Stack` child drops it). */
16
+ export declare function PageHeaderContent({ breadcrumbs, title, subtitle, actions, tabs, className, ...rest }: PageHeaderProps & Passthrough & {
17
+ className?: string;
18
+ }): ReactElement;
19
+ /**
20
+ * The top of a page: breadcrumbs, title, subtitle, actions and tabs. Its fixed bottom margin puts
21
+ * every page's first block at the same y; as a direct child of a `Stack` it has none, so the Stack's
22
+ * gap is the only space under it.
23
+ */
24
+ export declare function PageHeader(props: PageHeaderProps & Passthrough): ReactElement;
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { HeaderText, PageHeading } from "./typography.js";
3
+ /** The header block without its bottom margin; PageHeader adds the margin (a `Stack` child drops it). */
4
+ export function PageHeaderContent({ breadcrumbs, title, subtitle, actions, tabs, className, ...rest }) {
5
+ return (_jsxs("div", { ...rest, "data-page-header": "", className: className, children: [breadcrumbs && _jsx("div", { className: "mb-2", children: breadcrumbs }), _jsxs("div", { className: "flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between", children: [_jsxs("div", { className: "min-w-0", children: [_jsx(PageHeading, { children: title }), subtitle && (_jsx("div", { className: "mt-1", children: _jsx(HeaderText, { children: subtitle }) }))] }), actions && _jsx("div", { className: "flex shrink-0 flex-wrap items-center gap-2", children: actions })] }), tabs && _jsx("div", { className: "mt-4", children: tabs })] }));
6
+ }
7
+ /**
8
+ * The top of a page: breadcrumbs, title, subtitle, actions and tabs. Its fixed bottom margin puts
9
+ * every page's first block at the same y; as a direct child of a `Stack` it has none, so the Stack's
10
+ * gap is the only space under it.
11
+ */
12
+ export function PageHeader(props) {
13
+ return _jsx(PageHeaderContent, { ...props, className: "mb-8" });
14
+ }
@@ -0,0 +1,130 @@
1
+ import { type ComponentPropsWithoutRef, type ReactElement, type ReactNode } from "react";
2
+ import { type PageHeaderProps } from "./PageHeader.js";
3
+ import { type StackGap } from "./Stack.js";
4
+ import type { Passthrough, Unstyled } from "./props.js";
5
+ /** A list page: header, an optional `filters` slot, then the list, or `empty` when `isEmpty`. */
6
+ export declare function ListLayout({ filters, empty, isEmpty, children, breadcrumbs, title, subtitle, actions, tabs, ...rest }: PageHeaderProps & {
7
+ /** A row between the header and the list, e.g. a `FilterGroup` or a `SearchInput`. */
8
+ filters?: ReactNode;
9
+ /** Shown in place of `children` when `isEmpty`. */
10
+ empty?: ReactNode;
11
+ /** Show `empty` instead of `children`. */
12
+ isEmpty?: boolean;
13
+ children?: ReactNode;
14
+ } & Passthrough): ReactElement;
15
+ /** A detail page: header, then `children` (two thirds from `lg`) beside `aside` (one third), which stacks below on a smaller screen. */
16
+ export declare function DetailLayout({ aside, children, breadcrumbs, title, subtitle, actions, tabs, ...rest }: PageHeaderProps & {
17
+ /** The side column: one third from `lg`, below `children` on a smaller screen. */
18
+ aside: ReactNode;
19
+ children: ReactNode;
20
+ } & Passthrough): ReactElement;
21
+ /** A form page: header (no header `actions`), then a `max-w-3xl` column that starts at the Container's left edge (it is not re-centred), with an `actions` row at the end. Other props go on the `<form>`. */
22
+ export declare function FormLayout({ actions, children, breadcrumbs, title, subtitle, tabs, ...form }: Omit<PageHeaderProps, "actions"> & {
23
+ /** The row at the end of the form: its submit and cancel buttons. */
24
+ actions?: ReactNode;
25
+ children: ReactNode;
26
+ } & Unstyled<Omit<ComponentPropsWithoutRef<"form">, "title">>): ReactElement;
27
+ /**
28
+ * A text page: header, then `children` stacked `gap` apart (`md` by default), all in one `narrow`
29
+ * (`max-w-3xl`) reading column that starts at the Container's left edge (it is not re-centred, so
30
+ * the title does not jump). Unlike `FormLayout` the header sits inside the column, so its `actions`
31
+ * stay beside the text instead of at the Container's far edge.
32
+ */
33
+ export declare function ProseLayout({ gap, children, breadcrumbs, title, subtitle, actions, tabs, ...rest }: PageHeaderProps & {
34
+ /**
35
+ * The space between blocks: `xs` gap-2, `sm` gap-4, `md` gap-6, `lg` gap-8, `xl` gap-12 (gap-16 from `sm`).
36
+ * @default md
37
+ */
38
+ gap?: StackGap | undefined;
39
+ children?: ReactNode;
40
+ } & Passthrough): ReactElement;
41
+ declare const COLUMNS: {
42
+ readonly 2: "lg:grid-cols-2";
43
+ readonly 3: "lg:grid-cols-3";
44
+ readonly 4: "lg:grid-cols-4";
45
+ };
46
+ /** `columns="auto"` fits as many cards per row as the width allows, none narrower than this. */
47
+ declare const AUTO_MIN_WIDTH: {
48
+ readonly sm: "grid-cols-[repeat(auto-fill,minmax(12rem,1fr))]";
49
+ readonly md: "grid-cols-[repeat(auto-fill,minmax(17rem,1fr))]";
50
+ };
51
+ type CardGridProps = ({
52
+ /**
53
+ * Cards per row from `lg` (one on a phone, two from `sm`), or `auto` for as many as fit.
54
+ * @default 3
55
+ */
56
+ columns?: keyof typeof COLUMNS | undefined;
57
+ /** With `columns="auto"`, the narrowest a card gets: `md` 17rem or `sm` 12rem. */
58
+ minWidth?: undefined;
59
+ } | {
60
+ /**
61
+ * Cards per row from `lg` (one on a phone, two from `sm`), or `auto` for as many as fit.
62
+ * @default 3
63
+ */
64
+ columns: "auto";
65
+ /**
66
+ * With `columns="auto"`, the narrowest a card gets: `md` 17rem or `sm` 12rem.
67
+ * @default md
68
+ */
69
+ minWidth?: keyof typeof AUTO_MIN_WIDTH | undefined;
70
+ }) & {
71
+ /** `start` keeps each card its own height; the default stretches every card to the tallest in its row. */
72
+ align?: "stretch" | "start" | undefined;
73
+ /** `ul` makes the grid a list: each child is wrapped in an `<li>`. Default `div`. */
74
+ as?: "div" | "ul" | undefined;
75
+ children: ReactNode;
76
+ } & Passthrough;
77
+ /**
78
+ * A responsive grid of tiles: one column on a phone, two from `sm`, `columns` from `lg`.
79
+ * `columns="auto"` instead fits as many per row as the width allows, each at least `minWidth` wide:
80
+ * `md` (17rem, the default) or `sm` (12rem). `align="start"` keeps each card its own height
81
+ * instead of stretching it to the tallest in its row. `as="ul"` makes it a list of cards for a
82
+ * screen reader: the grid is a `<ul>` and each child is wrapped in an `<li>`, still stretched.
83
+ */
84
+ export declare function CardGrid({ columns, minWidth, align, as: Tag, children, ...rest }: CardGridProps): ReactElement;
85
+ /**
86
+ * A `rail` of links beside the `children` canvas, in the Container the shell already gives every
87
+ * page (a plain `<AppShell>`, no `bleed`): the rail starts at the header logo's x and the canvas
88
+ * ends at the header's right edge. The rail has no background and no border, so it is a list of
89
+ * links on the page; a `Tabs variant="pill" orientation="vertical"` is its content. From `md` it is `w-56` with
90
+ * the canvas filling the rest; below `md` it stacks above the canvas. A long rail sets `railLabel`:
91
+ * below `md` it then folds behind a disclosure of that name, closed, so the page comes first on a
92
+ * phone, and from `md` it is the same rail. Upgrading from 0.2: the rail no longer has a fill, a
93
+ * border or a fixed height, and it no longer sits beside the canvas on a phone.
94
+ */
95
+ export declare function SidebarLayout({ rail, railLabel, children, ...rest }: {
96
+ /** The links beside the canvas, usually a `Tabs variant="pill" orientation="vertical"`. */
97
+ rail: ReactNode;
98
+ /** Folds the rail behind a disclosure with this name below `md` (a long rail). Left out, the rail is always open. */
99
+ railLabel?: string | undefined;
100
+ children: ReactNode;
101
+ } & Passthrough): ReactElement;
102
+ /**
103
+ * Edge-to-edge `children` with an optional right `panel` (`lg:w-72`) that stacks below on a smaller
104
+ * screen. Render it in `<AppShell bleed>`. It deliberately breaks the shared left edge: a viewer
105
+ * that needs the whole width has no Container of its own, so `children` start at the viewport's
106
+ * left edge. `padded` gives them the Container's padding instead (`px-4`, `sm:px-6`, `py-8`), for
107
+ * a page of ordinary content beside a full-height `panel`. A `bar` is a `StickyBar` above both: the
108
+ * full width, on the surface with a bottom line, sticking under AppShell's header as the page scrolls.
109
+ */
110
+ export declare function FullBleedLayout({ bar, panel, padded, children, ...rest }: {
111
+ /** A full-width row above `children` and `panel` that sticks under the header, e.g. a viewer's title and actions. `padded` does not pad it. */
112
+ bar?: ReactNode;
113
+ /** A side column on the right (`lg:w-72`) on a tinted surface; below `lg` it stacks under `children`. */
114
+ panel?: ReactNode;
115
+ /**
116
+ * Pads `children` as the Container pads every other page. Off, they run to the viewport's edges.
117
+ * A notice in the column is a plain `Callout` in a `Stack`, never a `Callout banner`.
118
+ */
119
+ padded?: boolean | undefined;
120
+ children: ReactNode;
121
+ } & Passthrough): ReactElement;
122
+ /** A narrow card (`max-w-md`) centred in the Container at a fixed offset from the top, for sign-in, landing, not-found and error pages. The one layout that does not share the left edge. */
123
+ export declare function CenteredLayout({ title, subtitle, children, ...rest }: {
124
+ /** The card's `h1`. */
125
+ title: ReactNode;
126
+ /** A muted line under the title. */
127
+ subtitle?: ReactNode;
128
+ children?: ReactNode;
129
+ } & Passthrough): ReactElement;
130
+ export {};
@@ -0,0 +1,86 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Children, isValidElement, } from "react";
3
+ import { Card } from "./Card.js";
4
+ import { cx, focusRing } from "./cx.js";
5
+ import { PageHeader, PageHeaderContent } from "./PageHeader.js";
6
+ import { STACK_WIDTH, Stack } from "./Stack.js";
7
+ import { StickyBar } from "./StickyBar.js";
8
+ // Compositions of the layout foundation. None sets a page width or outer padding of its own; the
9
+ // ones that narrow their content do it inside the Container they are rendered in.
10
+ /** A list page: header, an optional `filters` slot, then the list, or `empty` when `isEmpty`. */
11
+ export function ListLayout({ filters, empty, isEmpty = false, children, breadcrumbs, title, subtitle, actions, tabs, ...rest }) {
12
+ return (_jsxs(_Fragment, { children: [_jsx(PageHeader, { breadcrumbs: breadcrumbs, title: title, subtitle: subtitle, actions: actions, tabs: tabs }), _jsxs(Stack, { ...rest, children: [filters, isEmpty ? empty : children] })] }));
13
+ }
14
+ /** A detail page: header, then `children` (two thirds from `lg`) beside `aside` (one third), which stacks below on a smaller screen. */
15
+ export function DetailLayout({ aside, children, breadcrumbs, title, subtitle, actions, tabs, ...rest }) {
16
+ return (_jsxs(_Fragment, { children: [_jsx(PageHeader, { breadcrumbs: breadcrumbs, title: title, subtitle: subtitle, actions: actions, tabs: tabs }), _jsxs("div", { ...rest, className: "grid gap-6 lg:grid-cols-3", children: [_jsx("div", { className: "min-w-0 lg:col-span-2", children: _jsx(Stack, { children: children }) }), _jsx("aside", { className: "min-w-0", children: _jsx(Stack, { children: aside }) })] })] }));
17
+ }
18
+ /** A form page: header (no header `actions`), then a `max-w-3xl` column that starts at the Container's left edge (it is not re-centred), with an `actions` row at the end. Other props go on the `<form>`. */
19
+ export function FormLayout({ actions, children, breadcrumbs, title, subtitle, tabs, ...form }) {
20
+ return (_jsxs(_Fragment, { children: [_jsx(PageHeader, { breadcrumbs: breadcrumbs, title: title, subtitle: subtitle, tabs: tabs }), _jsx("form", { ...form, className: "max-w-3xl", children: _jsxs(Stack, { children: [children, actions && _jsx("div", { className: "flex flex-wrap items-center gap-2", children: actions })] }) })] }));
21
+ }
22
+ /**
23
+ * A text page: header, then `children` stacked `gap` apart (`md` by default), all in one `narrow`
24
+ * (`max-w-3xl`) reading column that starts at the Container's left edge (it is not re-centred, so
25
+ * the title does not jump). Unlike `FormLayout` the header sits inside the column, so its `actions`
26
+ * stay beside the text instead of at the Container's far edge.
27
+ */
28
+ export function ProseLayout({ gap = "md", children, breadcrumbs, title, subtitle, actions, tabs, ...rest }) {
29
+ return (_jsxs("div", { ...rest, className: STACK_WIDTH.narrow, children: [_jsx(PageHeader, { breadcrumbs: breadcrumbs, title: title, subtitle: subtitle, actions: actions, tabs: tabs }), _jsx(Stack, { gap: gap, children: children })] }));
30
+ }
31
+ const COLUMNS = {
32
+ 2: "lg:grid-cols-2",
33
+ 3: "lg:grid-cols-3",
34
+ 4: "lg:grid-cols-4",
35
+ };
36
+ /** `columns="auto"` fits as many cards per row as the width allows, none narrower than this. */
37
+ const AUTO_MIN_WIDTH = {
38
+ sm: "grid-cols-[repeat(auto-fill,minmax(12rem,1fr))]",
39
+ md: "grid-cols-[repeat(auto-fill,minmax(17rem,1fr))]",
40
+ };
41
+ /**
42
+ * A responsive grid of tiles: one column on a phone, two from `sm`, `columns` from `lg`.
43
+ * `columns="auto"` instead fits as many per row as the width allows, each at least `minWidth` wide:
44
+ * `md` (17rem, the default) or `sm` (12rem). `align="start"` keeps each card its own height
45
+ * instead of stretching it to the tallest in its row. `as="ul"` makes it a list of cards for a
46
+ * screen reader: the grid is a `<ul>` and each child is wrapped in an `<li>`, still stretched.
47
+ */
48
+ export function CardGrid({ columns = 3, minWidth = "md", align = "stretch", as: Tag = "div", children, ...rest }) {
49
+ return (_jsx(Tag, { ...rest, ...(Tag === "ul" ? { role: "list" } : {}), className: cx("grid gap-4", columns === "auto"
50
+ ? AUTO_MIN_WIDTH[minWidth]
51
+ : cx("grid-cols-1 sm:grid-cols-2", COLUMNS[columns]), align === "start" && "items-start"), children: Tag === "ul"
52
+ ? Children.toArray(children).map((child, index) => (_jsx("li", { className: "grid", children: child }, isValidElement(child) ? child.key : index)))
53
+ : children }));
54
+ }
55
+ /**
56
+ * A `rail` of links beside the `children` canvas, in the Container the shell already gives every
57
+ * page (a plain `<AppShell>`, no `bleed`): the rail starts at the header logo's x and the canvas
58
+ * ends at the header's right edge. The rail has no background and no border, so it is a list of
59
+ * links on the page; a `Tabs variant="pill" orientation="vertical"` is its content. From `md` it is `w-56` with
60
+ * the canvas filling the rest; below `md` it stacks above the canvas. A long rail sets `railLabel`:
61
+ * below `md` it then folds behind a disclosure of that name, closed, so the page comes first on a
62
+ * phone, and from `md` it is the same rail. Upgrading from 0.2: the rail no longer has a fill, a
63
+ * border or a fixed height, and it no longer sits beside the canvas on a phone.
64
+ */
65
+ export function SidebarLayout({ rail, railLabel, children, ...rest }) {
66
+ return (_jsxs("div", { ...rest, className: "flex flex-col gap-8 md:flex-row md:gap-10", children: [_jsx("aside", { className: "w-full shrink-0 md:w-56", children: railLabel === undefined ? (rail) : (_jsxs("details", { className: "group md:[&::details-content]:[content-visibility:visible]", children: [_jsxs("summary", { className: cx("flex cursor-pointer list-none items-center justify-between rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm font-medium text-slate-900 select-none hover:bg-slate-50 md:hidden [&::-webkit-details-marker]:hidden", focusRing), children: [railLabel, _jsx("svg", { viewBox: "0 0 20 20", width: "16", height: "16", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", className: "shrink-0 text-slate-500 group-open:rotate-180", children: _jsx("path", { d: "M5 8l5 5 5-5" }) })] }), _jsx("div", { className: "mt-2 md:mt-0", children: rail })] })) }), _jsx("div", { className: "min-w-0 flex-1", children: children })] }));
67
+ }
68
+ /**
69
+ * Edge-to-edge `children` with an optional right `panel` (`lg:w-72`) that stacks below on a smaller
70
+ * screen. Render it in `<AppShell bleed>`. It deliberately breaks the shared left edge: a viewer
71
+ * that needs the whole width has no Container of its own, so `children` start at the viewport's
72
+ * left edge. `padded` gives them the Container's padding instead (`px-4`, `sm:px-6`, `py-8`), for
73
+ * a page of ordinary content beside a full-height `panel`. A `bar` is a `StickyBar` above both: the
74
+ * full width, on the surface with a bottom line, sticking under AppShell's header as the page scrolls.
75
+ */
76
+ export function FullBleedLayout({ bar, panel, padded = false, children, ...rest }) {
77
+ const row = (_jsxs(_Fragment, { children: [_jsx("div", { className: cx("min-w-0 flex-1", padded && "px-4 py-8 sm:px-6"), children: children }), panel && (_jsx("aside", { className: "w-full shrink-0 border-t border-slate-200 bg-slate-50 p-4 lg:w-72 lg:border-t-0 lg:border-l", children: panel }))] }));
78
+ if (!bar) {
79
+ return (_jsx("div", { ...rest, className: "flex min-h-0 flex-1 flex-col lg:flex-row", children: row }));
80
+ }
81
+ return (_jsxs("div", { ...rest, className: "flex min-h-0 flex-1 flex-col", children: [_jsx(StickyBar, { children: bar }), _jsx("div", { className: "flex min-h-0 flex-1 flex-col lg:flex-row", children: row })] }));
82
+ }
83
+ /** A narrow card (`max-w-md`) centred in the Container at a fixed offset from the top, for sign-in, landing, not-found and error pages. The one layout that does not share the left edge. */
84
+ export function CenteredLayout({ title, subtitle, children, ...rest }) {
85
+ return (_jsx("div", { ...rest, className: "mx-auto w-full max-w-md pt-16", children: _jsx(Card, { as: "div", children: _jsxs(Stack, { children: [_jsx(PageHeaderContent, { title: title, subtitle: subtitle }), children] }) }) }));
86
+ }
package/dist/Pill.d.ts ADDED
@@ -0,0 +1,53 @@
1
+ import type { MouseEventHandler, ReactElement, ReactNode } from "react";
2
+ import { type LinkRender } from "./link.js";
3
+ import type { Passthrough } from "./props.js";
4
+ export type PillTone = "slate" | "amber" | "emerald" | "blue" | "red" | "violet" | "sky";
5
+ type PillBase = {
6
+ children: ReactNode;
7
+ /**
8
+ * The tint: `slate`, `amber`, `emerald`, `blue`, `red`, `violet` or `sky`.
9
+ * @default slate
10
+ */
11
+ tone?: PillTone;
12
+ /** A leading dot in the pill's own text colour. */
13
+ dot?: boolean;
14
+ /** A full pill shape in place of the rounded-md corners. */
15
+ round?: boolean;
16
+ /** The tooltip on hover, for a pill whose text is short or cut off: the full status, a date. */
17
+ title?: string | undefined;
18
+ };
19
+ type PillProps = PillBase & Passthrough & ({
20
+ href?: undefined;
21
+ render?: undefined;
22
+ external?: undefined;
23
+ onClick?: undefined;
24
+ } | {
25
+ /** A Pill that is a button: runs `onClick`. Defaults to `type="button"`. */
26
+ onClick: MouseEventHandler<HTMLButtonElement>;
27
+ href?: undefined;
28
+ render?: undefined;
29
+ external?: undefined;
30
+ } | ({
31
+ onClick?: undefined;
32
+ /** Opens in a new tab: adds `target="_blank"` and `rel="noreferrer"`. */
33
+ external?: boolean | undefined;
34
+ } & ({
35
+ /** Makes the pill a link to this URL. */
36
+ href: string;
37
+ /** Draws the link with the app's own router `<Link>`: `(p) => <Link to="/flakes" {...p} />`. */
38
+ render?: LinkRender | undefined;
39
+ } | {
40
+ /** Makes the pill a link to this URL. */
41
+ href?: string | undefined;
42
+ /** Draws the link with the app's own router `<Link>`: `(p) => <Link to="/flakes" {...p} />`. */
43
+ render: LinkRender;
44
+ })));
45
+ /**
46
+ * A small tinted label; `dot` adds a leading dot in the pill's own text colour, `round` swaps the
47
+ * rounded-md corners for a full pill shape. `href` makes it a link (a plain `<a>`, or with `render`
48
+ * the app's own router `<Link>`: `render={(p) => <Link to="/flakes" {...p} />}`; `external` opens a
49
+ * new tab) and `onClick` makes it a button, each with a hover dim and the focus ring. `title` is
50
+ * the native tooltip.
51
+ */
52
+ export declare function Pill({ children, tone, dot, round, href, render, external, onClick, ...rest }: PillProps): ReactElement;
53
+ export {};
package/dist/Pill.js ADDED
@@ -0,0 +1,34 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx, focusRing } from "./cx.js";
3
+ import { renderLink } from "./link.js";
4
+ const PILL_TONES = {
5
+ slate: "bg-slate-100 text-slate-600",
6
+ amber: "bg-amber-50 text-amber-700",
7
+ emerald: "bg-emerald-50 text-emerald-700",
8
+ blue: "bg-blue-50 text-blue-700",
9
+ red: "bg-red-50 text-red-900",
10
+ violet: "bg-violet-50 text-violet-700",
11
+ sky: "bg-sky-50 text-sky-700",
12
+ };
13
+ function pillClass({ tone, round, dot }, interactive) {
14
+ return cx(round ? "rounded-full" : "rounded-md", "px-1.5 py-0.5 text-xs font-medium", PILL_TONES[tone], dot && "inline-flex items-center gap-1", interactive && cx("transition hover:opacity-80", focusRing));
15
+ }
16
+ /**
17
+ * A small tinted label; `dot` adds a leading dot in the pill's own text colour, `round` swaps the
18
+ * rounded-md corners for a full pill shape. `href` makes it a link (a plain `<a>`, or with `render`
19
+ * the app's own router `<Link>`: `render={(p) => <Link to="/flakes" {...p} />}`; `external` opens a
20
+ * new tab) and `onClick` makes it a button, each with a hover dim and the focus ring. `title` is
21
+ * the native tooltip.
22
+ */
23
+ export function Pill({ children, tone = "slate", dot = false, round = false, href, render, external = false, onClick, ...rest }) {
24
+ const isLink = href !== undefined || render !== undefined;
25
+ const className = pillClass({ tone, round, dot }, isLink || onClick !== undefined);
26
+ const content = (_jsxs(_Fragment, { children: [dot && _jsx("span", { "aria-hidden": "true", className: "h-1.5 w-1.5 rounded-full bg-current" }), children] }));
27
+ if (isLink) {
28
+ return renderLink({ ...rest, href, render, external, className, children: content });
29
+ }
30
+ if (onClick) {
31
+ return (_jsx("button", { type: "button", ...rest, onClick: onClick, className: cx(className, "cursor-pointer"), children: content }));
32
+ }
33
+ return (_jsx("span", { ...rest, className: className, children: content }));
34
+ }
@@ -0,0 +1,32 @@
1
+ import type { ReactElement } from "react";
2
+ import type { Passthrough } from "./props.js";
3
+ import type { Size } from "./size.js";
4
+ /** The colour of a bar's fill. */
5
+ export type BarTone = "accent" | "positive" | "warn" | "negative";
6
+ /** 6px, 8px or 12px tall. */
7
+ export type BarSize = Size;
8
+ export declare const BAR_FILL: Record<BarTone, string>;
9
+ export declare const BAR_TRACK: Record<BarSize, string>;
10
+ /** `part` as a percent of `whole`, held to 0–100; 0 when either is not positive. */
11
+ export declare function sharePercent(part: number, whole: number): number;
12
+ /** The share as a whole percent for a screen reader: never 0 or 100 unless it truly is. */
13
+ export declare function announcedPercent(part: number, whole: number): number;
14
+ /**
15
+ * A thin horizontal bar with a filled share, for progress toward a total. `value` is held to
16
+ * 0–`max`. `label` names what it measures, for a screen reader, which also hears the percentage.
17
+ */
18
+ export declare function ProgressBar({ value, max, label, tone, size, ...rest }: {
19
+ /** How far along, held to 0–`max`. */
20
+ value: number;
21
+ /** Defaults to 100. */
22
+ max?: number;
23
+ /** What the bar measures, for a screen reader, which also hears the percentage. */
24
+ label: string;
25
+ /** Defaults to "accent". */
26
+ tone?: BarTone;
27
+ /**
28
+ * The bar's height: `sm` 6px, `md` 8px, `lg` 12px.
29
+ * @default md
30
+ */
31
+ size?: BarSize;
32
+ } & Passthrough): ReactElement;
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cx } from "./cx.js";
3
+ export const BAR_FILL = {
4
+ accent: "bg-violet-600",
5
+ positive: "bg-emerald-600",
6
+ warn: "bg-amber-600",
7
+ negative: "bg-red-600",
8
+ };
9
+ export const BAR_TRACK = {
10
+ sm: "h-1.5",
11
+ md: "h-2",
12
+ lg: "h-3",
13
+ };
14
+ /** `part` as a percent of `whole`, held to 0–100; 0 when either is not positive. */
15
+ export function sharePercent(part, whole) {
16
+ if (!(whole > 0) || !(part > 0))
17
+ return 0;
18
+ return Math.min(100, (part / whole) * 100);
19
+ }
20
+ /** The share as a whole percent for a screen reader: never 0 or 100 unless it truly is. */
21
+ export function announcedPercent(part, whole) {
22
+ const share = sharePercent(part, whole);
23
+ return share <= 0 || share >= 100 ? share : Math.min(99, Math.max(1, Math.round(share)));
24
+ }
25
+ /**
26
+ * A thin horizontal bar with a filled share, for progress toward a total. `value` is held to
27
+ * 0–`max`. `label` names what it measures, for a screen reader, which also hears the percentage.
28
+ */
29
+ export function ProgressBar({ value, max = 100, label, tone = "accent", size = "md", ...rest }) {
30
+ const share = sharePercent(value, max);
31
+ return (_jsx("div", { ...rest, role: "progressbar", "aria-label": label, "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": announcedPercent(value, max), className: cx("overflow-hidden rounded-full bg-slate-100", BAR_TRACK[size]), children: _jsx("div", { className: cx("h-full", BAR_FILL[tone]), style: { width: `${share}%` } }) }));
32
+ }
@@ -0,0 +1,48 @@
1
+ import { type AriaAttributes, type ReactElement, type ReactNode } from "react";
2
+ import { type RadioGroupProps as AriaRadioGroupProps } from "react-aria-components";
3
+ import type { DataAttributes } from "./link.js";
4
+ import type { Size } from "./size.js";
5
+ /**
6
+ * A named group of radios, one chosen. The `label` names the group; an `error` is a red line under
7
+ * it with `role="alert"` and marks the group invalid. Give it `value` and `onChange`, or
8
+ * `defaultValue` and let it keep the choice. `name` posts the choice in a form.
9
+ * The arrow keys move between the radios and choose, and the group is one tab stop. `size` sizes
10
+ * every radio in the group, the same scale as a `Checkbox`.
11
+ */
12
+ export declare function RadioGroup({ label, error, size, children, ...rest }: {
13
+ /** The group's visible name, above the options. */
14
+ label: ReactNode;
15
+ /** Shown under the options in red with `role="alert"`; marks the group invalid. Like `Field`'s `error`. */
16
+ error?: ReactNode;
17
+ /** The `value` of the chosen radio. */
18
+ value?: string;
19
+ /** The `value` chosen to begin with, when the group keeps its own choice. */
20
+ defaultValue?: string;
21
+ /** Called with the chosen radio's `value`. */
22
+ onChange?: (value: string) => void;
23
+ /** The form field the choice posts under. */
24
+ name?: string;
25
+ /** Disables every radio. */
26
+ isDisabled?: boolean;
27
+ /** Marks the choice required: the group is `aria-required`, and a form will not submit with no option chosen. */
28
+ isRequired?: boolean;
29
+ /**
30
+ * The size of every radio in the group: `sm` is `text-xs` and a smaller circle, `lg` `text-base`
31
+ * and a larger one.
32
+ * @default md
33
+ */
34
+ size?: Size;
35
+ /** The `Radio`s. */
36
+ children: ReactNode;
37
+ } & DataAttributes & Pick<AriaRadioGroupProps, "aria-label" | "aria-labelledby" | "aria-describedby" | "aria-details">): ReactElement;
38
+ /** One option of a `RadioGroup`: a `label` and, under it, an optional `description`. Click the label or the circle. */
39
+ export declare function Radio({ label, description, value, isDisabled, "aria-describedby": describedBy, ...rest }: {
40
+ /** The text beside the circle. */
41
+ label: ReactNode;
42
+ /** A muted line under the label that says more. It is the radio's `aria-describedby`. */
43
+ description?: ReactNode;
44
+ /** What the group's `value` becomes when this one is chosen. */
45
+ value: string;
46
+ /** Disables this radio alone. */
47
+ isDisabled?: boolean;
48
+ } & DataAttributes & Pick<AriaAttributes, "aria-describedby">): ReactElement;