@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.
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 +5 -0
  137. package/dist/Textarea.js +7 -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 +10 -0
  147. package/dist/cx.js +12 -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,100 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { CalendarDate as CalendarDateClass, DateFormatter, } from "@internationalized/date";
3
+ import { useContext, useEffect } from "react";
4
+ import { DateField as AriaDateField, DateFieldStateContext, DateInput, DateSegment, FieldError, I18nProvider, Label, Text, } from "react-aria-components";
5
+ import { cx, focusRing } from "./cx.js";
6
+ import { CalendarDateField } from "./DateFieldCalendar.js";
7
+ import { toISODate } from "./isoDate.js";
8
+ import { ShownInput, useShownDate } from "./shownDate.js";
9
+ /** Fixed, whatever the browser's locale: DD/MM/YYYY and Monday-first weeks. */
10
+ export const LOCALE = "en-GB";
11
+ const MONTHS = new DateFormatter(LOCALE, { month: "long", timeZone: "UTC" });
12
+ export const MONTH_OPTIONS = Array.from({ length: 12 }, (_, i) => ({
13
+ month: i + 1,
14
+ name: MONTHS.format(new Date(Date.UTC(2000, i, 1))),
15
+ }));
16
+ function readSegments(segments) {
17
+ const editable = segments.filter((segment) => segment.isEditable);
18
+ const blank = editable.filter((segment) => segment.isPlaceholder).length;
19
+ const none = { typed: "", impossible: null, partial: blank > 0 && blank < editable.length };
20
+ if (editable.length === 0 || blank > 0)
21
+ return none;
22
+ const part = (type) => editable.find((s) => s.type === type)?.value ?? 0;
23
+ const [day, month, year] = [part("day"), part("month"), part("year")];
24
+ if (day < 1 || month < 1 || year < 1)
25
+ return none;
26
+ const first = new CalendarDateClass(year, month, 1);
27
+ const days = first.calendar.getDaysInMonth(first);
28
+ if (day <= days)
29
+ return { ...none, typed: toISODate(first.set({ day })) };
30
+ return {
31
+ ...none,
32
+ impossible: `${MONTH_OPTIONS[month - 1]?.name ?? ""} ${year} has ${days} days.`,
33
+ };
34
+ }
35
+ /** One segment. The day segment also reports what the segments show to the field. */
36
+ export function FieldSegment({ segment, onRead, }) {
37
+ const state = useContext(DateFieldStateContext);
38
+ const { typed, impossible, partial } = readSegments(state?.segments ?? []);
39
+ useEffect(() => onRead?.({ typed, impossible, partial }), [typed, impossible, partial, onRead]);
40
+ return (_jsx(DateSegment, { segment: segment, className: ({ isFocused, isPlaceholder, isInvalid, type }) => {
41
+ if (type === "literal")
42
+ return "text-slate-400";
43
+ let tone = "text-slate-900";
44
+ if (isFocused)
45
+ tone = "bg-violet-50 text-violet-600";
46
+ else if (isPlaceholder)
47
+ tone = "text-slate-400";
48
+ else if (isInvalid)
49
+ tone = "text-red-700";
50
+ return cx("rounded px-0.5 tabular-nums outline-none", tone);
51
+ }, children: ({ isPlaceholder, placeholder, text }) => (isPlaceholder ? placeholder.toUpperCase() : text) }));
52
+ }
53
+ export function frameClass({ isInvalid, isFocusWithin, isDisabled, }) {
54
+ let tone = "border-slate-300 bg-white text-slate-900 hover:border-violet-400";
55
+ if (isInvalid)
56
+ tone = "border-red-200 bg-red-50 text-red-700";
57
+ else if (isFocusWithin)
58
+ tone = "border-violet-600 bg-white text-slate-900";
59
+ return cx("flex w-full items-center rounded-lg border transition", tone, isDisabled && "opacity-50");
60
+ }
61
+ /** The props that force a field invalid: an impossible date, `isInvalid`, or an `error` from outside. Spread onto the React Aria field after the rest. */
62
+ export function forcedInvalid({ impossible, isInvalid, error, }) {
63
+ return impossible || isInvalid || error ? { isInvalid: true } : {};
64
+ }
65
+ /** The label, the description or the error line: the shared outside of both components. */
66
+ export function Notes({ description, errorMessage, error, impossible, isInvalid, }) {
67
+ return (_jsxs(_Fragment, { children: [!isInvalid && description ? (_jsx(Text, { slot: "description", className: "text-xs text-slate-500", children: description })) : null, _jsx(FieldError, { className: "text-xs text-red-700", children: ({ validationErrors }) => error ? (_jsx("span", { role: "alert", children: error })) : ((errorMessage ?? impossible ?? validationErrors.join(" "))) })] }));
68
+ }
69
+ /** A `DateField`'s padding and text, by `size`: the heights of an `Input`. */
70
+ const FIELD_SIZE = {
71
+ sm: "py-1 pl-2 pr-2 text-xs",
72
+ md: "py-2 pl-2.5 pr-2.5 text-sm",
73
+ lg: "py-2.5 pl-3 pr-3 text-base",
74
+ };
75
+ export const labelClass = "text-sm font-medium text-slate-700";
76
+ /** A small icon button inside a date field or calendar, without its box size. */
77
+ export const iconButtonLook = `inline-flex shrink-0 items-center justify-center rounded-md text-slate-500 transition hover:bg-slate-100 disabled:opacity-50 ${focusRing}`;
78
+ export const iconButtonClass = `${iconButtonLook} h-7 w-7`;
79
+ /**
80
+ * A segmented DD / MM / YYYY date input. Values are `CalendarDate`s; `name` posts it as
81
+ * `YYYY-MM-DD`. `calendar` adds a calendar button that opens a month view in a popover, at the
82
+ * same height as the plain field; `defaultOpen` opens it on mount.
83
+ */
84
+ export function DateField({ calendar, defaultOpen, ...field }) {
85
+ if (calendar) {
86
+ return _jsx(CalendarDateField, { ...field, ...(defaultOpen === undefined ? {} : { defaultOpen }) });
87
+ }
88
+ return _jsx(PlainDateField, { ...field });
89
+ }
90
+ function PlainDateField({ label, description, errorMessage, error, isInvalid: invalidProp, value, defaultValue, onChange, name, size = "md", ...rest }) {
91
+ const { inner, setInner, shown, impossible, onRead, validate, leaving, onBlurCapture, onLeft } = useShownDate({
92
+ value,
93
+ defaultValue,
94
+ onChange,
95
+ minValue: rest.minValue,
96
+ maxValue: rest.maxValue,
97
+ isRequired: rest.isRequired,
98
+ });
99
+ return (_jsx(I18nProvider, { locale: LOCALE, children: _jsx(AriaDateField, { ...rest, value: inner, onChange: setInner, validate: validate, ...forcedInvalid({ impossible, isInvalid: invalidProp, error }), isReadOnly: leaving, onBlur: onLeft, granularity: "day", shouldForceLeadingZeros: true, className: "flex flex-col gap-1.5", children: ({ isInvalid }) => (_jsxs(_Fragment, { children: [label ? _jsx(Label, { className: labelClass, children: label }) : null, _jsx("div", { className: "contents", onBlurCapture: onBlurCapture, children: _jsx(DateInput, { className: (state) => cx(frameClass(state), FIELD_SIZE[size]), children: (segment) => (_jsx(FieldSegment, { segment: segment, ...(segment.type === "day" ? { onRead } : {}) })) }) }), _jsx(ShownInput, { name: name, shown: shown, isDisabled: rest.isDisabled }), _jsx(Notes, { description: description, errorMessage: errorMessage, error: error, impossible: impossible, isInvalid: isInvalid })] })) }) }));
100
+ }
@@ -0,0 +1,6 @@
1
+ import { type ReactElement } from "react";
2
+ import { type FieldProps } from "./DateField.js";
3
+ /** The body of `<DateField calendar>`: the field with a calendar button that opens a month view in a popover. */
4
+ export declare function CalendarDateField({ label, description, errorMessage, error, isInvalid: invalidProp, value, defaultValue, onChange, name, size, ...rest }: FieldProps & {
5
+ defaultOpen?: boolean;
6
+ }): ReactElement;
@@ -0,0 +1,35 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useContext, useState } from "react";
3
+ import { Button, DatePicker as AriaDatePicker, DateFieldContext, DateInput, Group, I18nProvider, Label, } from "react-aria-components";
4
+ import { cx } from "./cx.js";
5
+ import { CalendarPopover, calendarIcon } from "./DateCalendar.js";
6
+ import { FieldSegment, LOCALE, forcedInvalid, Notes, frameClass, iconButtonLook, labelClass, } from "./DateField.js";
7
+ import { ShownInput, useShownDate } from "./shownDate.js";
8
+ /** The picker's padding, text and calendar button, by `size`. The button sets the height, so the padding is less than a `DateField`'s. */
9
+ const PICKER_SIZE = {
10
+ sm: { frame: "py-0.5 pl-2 pr-0.5 text-xs", button: "h-5 w-5" },
11
+ md: { frame: "py-1 pl-2.5 pr-1 text-sm", button: "h-7 w-7" },
12
+ lg: { frame: "py-1.5 pl-3 pr-1.5 text-base", button: "h-8 w-8" },
13
+ };
14
+ /**
15
+ * The picker's input, read-only while `leaving`. Only the field: a read-only picker would also
16
+ * disable the calendar button that focus may be moving to.
17
+ */
18
+ function PickerInput({ leaving, onLeft, children, }) {
19
+ const field = useContext(DateFieldContext);
20
+ return (_jsx(DateFieldContext.Provider, { value: { ...field, isReadOnly: leaving, onBlur: onLeft }, children: children }));
21
+ }
22
+ /** The body of `<DateField calendar>`: the field with a calendar button that opens a month view in a popover. */
23
+ export function CalendarDateField({ label, description, errorMessage, error, isInvalid: invalidProp, value, defaultValue, onChange, name, size = "md", ...rest }) {
24
+ const { inner, setInner, shown, impossible, onRead, validate, leaving, onBlurCapture, onLeft } = useShownDate({
25
+ value,
26
+ defaultValue,
27
+ onChange,
28
+ minValue: rest.minValue,
29
+ maxValue: rest.maxValue,
30
+ isRequired: rest.isRequired,
31
+ });
32
+ const [dialog, setDialog] = useState(undefined);
33
+ const findDialog = useCallback((el) => setDialog(el?.closest("dialog") ?? undefined), []);
34
+ return (_jsx(I18nProvider, { locale: LOCALE, children: _jsx(AriaDatePicker, { ...rest, ref: findDialog, value: inner, onChange: setInner, validate: validate, ...forcedInvalid({ impossible, isInvalid: invalidProp, error }), granularity: "day", shouldForceLeadingZeros: true, className: "flex flex-col gap-1.5", children: ({ isInvalid }) => (_jsxs(_Fragment, { children: [label ? _jsx(Label, { className: labelClass, children: label }) : null, _jsxs(Group, { className: (state) => cx(frameClass(state), "gap-1", PICKER_SIZE[size].frame), children: [_jsx("div", { className: "contents", onBlurCapture: onBlurCapture, children: _jsx(PickerInput, { leaving: leaving, onLeft: onLeft, children: _jsx(DateInput, { className: "flex flex-1", children: (segment) => (_jsx(FieldSegment, { segment: segment, ...(segment.type === "day" ? { onRead } : {}) })) }) }) }), _jsx(Button, { className: cx(iconButtonLook, PICKER_SIZE[size].button), children: calendarIcon })] }), _jsx(ShownInput, { name: name, shown: shown, isDisabled: rest.isDisabled }), _jsx(Notes, { description: description, errorMessage: errorMessage, error: error, impossible: impossible, isInvalid: isInvalid }), _jsx(CalendarPopover, { container: dialog })] })) }) }));
35
+ }
@@ -0,0 +1,26 @@
1
+ import { type ReactElement, type ReactNode } from "react";
2
+ import type { Passthrough } from "./props.js";
3
+ import type { Size } from "./size.js";
4
+ export type DialogProps = {
5
+ /** The dialog's accessible name, shown as its heading. */
6
+ title: ReactNode;
7
+ /** A muted line under the title. */
8
+ description?: ReactNode;
9
+ children?: ReactNode;
10
+ /** Actions under the body, right-aligned. A submit button here can reach a form in the body with its `form` attribute. */
11
+ footer?: ReactNode;
12
+ /**
13
+ * The panel's width: `sm` (320px) is a list, `md` (440px) holds a form, `lg` (640px) a wider
14
+ * form or a table.
15
+ * @default md
16
+ */
17
+ size?: Size;
18
+ /** Called when the dialog closes: Esc, a click on the backdrop, or a `<form method="dialog">` submit. */
19
+ onClose: () => void;
20
+ } & Passthrough;
21
+ /**
22
+ * A modal on the browser's own `<dialog>`: it is opened with `showModal()` on mount, so render it
23
+ * only while it is open. The page behind goes inert, Esc closes it and focus stays inside; a click
24
+ * on the backdrop (the `<dialog>` element itself, not its contents) closes it too.
25
+ */
26
+ export declare function Dialog({ title, description, children, footer, size, onClose, ...rest }: DialogProps): ReactElement;
package/dist/Dialog.js ADDED
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId, useRef } from "react";
3
+ import { cx } from "./cx.js";
4
+ import { useModal } from "./modal.js";
5
+ import { HeaderText, SectionHeading } from "./typography.js";
6
+ const PANEL = "mx-auto mt-28 max-w-[calc(100vw-2rem)] overflow-visible rounded-2xl border border-slate-200 bg-white p-0 text-slate-900 shadow-2xl -translate-y-2 opacity-0 transition-[opacity,translate,overlay,display] transition-discrete duration-200 open:translate-y-0 open:opacity-100 starting:open:-translate-y-2 starting:open:opacity-0 backdrop:bg-slate-900/35 backdrop:opacity-0 backdrop:transition-[opacity,overlay,display] backdrop:transition-discrete backdrop:duration-200 open:backdrop:opacity-100 starting:open:backdrop:opacity-0 motion-reduce:transition-none motion-reduce:backdrop:transition-none";
7
+ const SIZE = { sm: "w-80", md: "w-110", lg: "w-160" };
8
+ /**
9
+ * A modal on the browser's own `<dialog>`: it is opened with `showModal()` on mount, so render it
10
+ * only while it is open. The page behind goes inert, Esc closes it and focus stays inside; a click
11
+ * on the backdrop (the `<dialog>` element itself, not its contents) closes it too.
12
+ */
13
+ export function Dialog({ title, description, children, footer, size = "md", onClose, ...rest }) {
14
+ const dialog = useRef(null);
15
+ const titleId = useId();
16
+ const closed = useModal(dialog, onClose);
17
+ return (_jsx("dialog", { ...rest, ref: dialog, "aria-labelledby": titleId, onClose: closed, onClick: (event) => event.target === dialog.current && dialog.current?.close(), className: cx(PANEL, SIZE[size]), children: _jsxs("div", { className: "p-5.5", children: [_jsx(SectionHeading, { id: titleId, children: title }), description && (_jsx("div", { className: "mt-1", children: _jsx(HeaderText, { children: description }) })), children && _jsx("div", { className: "mt-4", children: children }), footer && _jsx("div", { className: "mt-5 flex justify-end gap-2", children: footer })] }) }));
18
+ }
@@ -0,0 +1,15 @@
1
+ import type { ReactElement, ReactNode } from "react";
2
+ import type { Passthrough } from "./props.js";
3
+ /**
4
+ * A summary line with a chevron over a body that opens and closes under it. The summary is a
5
+ * button with `aria-expanded`, and Enter and Space toggle it; the body is hidden while closed.
6
+ * It starts closed; `defaultOpen` starts it open. It keeps its own state.
7
+ */
8
+ export declare function Disclosure({ summary, defaultOpen, children, ...rest }: {
9
+ /** The always-visible line: the button that toggles the body. */
10
+ summary: ReactNode;
11
+ /** Starts with the body showing. */
12
+ defaultOpen?: boolean | undefined;
13
+ /** The body, shown while open. */
14
+ children: ReactNode;
15
+ } & Passthrough): ReactElement;
@@ -0,0 +1,12 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button, Disclosure as AriaDisclosure, DisclosurePanel } from "react-aria-components";
3
+ import { Chevron } from "./Chevron.js";
4
+ import { focusRing } from "./cx.js";
5
+ /**
6
+ * A summary line with a chevron over a body that opens and closes under it. The summary is a
7
+ * button with `aria-expanded`, and Enter and Space toggle it; the body is hidden while closed.
8
+ * It starts closed; `defaultOpen` starts it open. It keeps its own state.
9
+ */
10
+ export function Disclosure({ summary, defaultOpen = false, children, ...rest }) {
11
+ return (_jsx("div", { ...rest, children: _jsx(AriaDisclosure, { defaultExpanded: defaultOpen, children: ({ isExpanded }) => (_jsxs(_Fragment, { children: [_jsxs(Button, { slot: "trigger", className: `flex w-full cursor-pointer items-center gap-2 rounded-md py-1.5 text-left text-sm font-medium text-slate-900 hover:text-violet-700 ${focusRing}`, children: [_jsx(Chevron, { open: isExpanded }), summary] }), _jsx(DisclosurePanel, { className: "pt-1 pb-2 pl-6 text-sm text-slate-700", children: children })] })) }) }));
12
+ }
@@ -0,0 +1,4 @@
1
+ import type { HTMLAttributes, ReactElement } from "react";
2
+ import type { Unstyled } from "./props.js";
3
+ /** A hairline rule in the theme's line colour: between a heading and its content, or above a footer. Space around it is the layout's. */
4
+ export declare function Divider(props: Unstyled<Omit<HTMLAttributes<HTMLHRElement>, "children">>): ReactElement;
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /** A hairline rule in the theme's line colour: between a heading and its content, or above a footer. Space around it is the layout's. */
3
+ export function Divider(props) {
4
+ return _jsx("hr", { ...props, className: "border-t border-slate-200/70" });
5
+ }
@@ -0,0 +1,29 @@
1
+ import { type ReactElement, type ReactNode } from "react";
2
+ import type { Passthrough } from "./props.js";
3
+ declare const SIZE: {
4
+ readonly sm: "w-80";
5
+ readonly md: "w-110";
6
+ readonly lg: "w-160";
7
+ };
8
+ export type DrawerProps = {
9
+ /** The drawer's accessible name, shown as its heading. */
10
+ title: ReactNode;
11
+ children?: ReactNode;
12
+ /** Actions pinned under the scrolling body, right-aligned. */
13
+ footer?: ReactNode;
14
+ /** `md` (the default) holds a form or a record; `sm` is narrower and `lg` wider. */
15
+ size?: keyof typeof SIZE;
16
+ /** Called when the drawer closes: Esc, a click on the scrim or the close button. */
17
+ onClose: () => void;
18
+ /** The accessible name of the close button. */
19
+ closeLabel?: string;
20
+ } & Passthrough;
21
+ /**
22
+ * A modal slide-over anchored to the right edge, on the browser's own `<dialog>`: it is opened
23
+ * with `showModal()` on mount, so render it only while it is open. The page behind goes inert, Esc
24
+ * closes it and focus stays inside; a click on the scrim (the `<dialog>` element itself, not its
25
+ * contents) closes it too. The header holds the title and a close button; the body scrolls between
26
+ * it and an optional pinned `footer`.
27
+ */
28
+ export declare function Drawer({ title, children, footer, size, onClose, closeLabel, ...rest }: DrawerProps): ReactElement;
29
+ export {};
package/dist/Drawer.js ADDED
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId, useRef } from "react";
3
+ import { cx } from "./cx.js";
4
+ import { Icon } from "./Icon.js";
5
+ import { IconButton } from "./IconButton.js";
6
+ import { useModal } from "./modal.js";
7
+ import { SectionHeading } from "./typography.js";
8
+ const PANEL = "fixed inset-y-0 right-0 left-auto m-0 h-dvh max-h-none max-w-full flex-col overflow-hidden border-l border-slate-200 bg-white p-0 text-slate-900 shadow-2xl translate-x-full transition-[translate,overlay,display] transition-discrete duration-200 open:flex open:translate-x-0 starting:open:translate-x-full backdrop:bg-slate-900/35 backdrop:opacity-0 backdrop:transition-[opacity,overlay,display] backdrop:transition-discrete backdrop:duration-200 open:backdrop:opacity-100 starting:open:backdrop:opacity-0 motion-reduce:transition-none motion-reduce:backdrop:transition-none";
9
+ const SIZE = { sm: "w-80", md: "w-110", lg: "w-160" };
10
+ /**
11
+ * A modal slide-over anchored to the right edge, on the browser's own `<dialog>`: it is opened
12
+ * with `showModal()` on mount, so render it only while it is open. The page behind goes inert, Esc
13
+ * closes it and focus stays inside; a click on the scrim (the `<dialog>` element itself, not its
14
+ * contents) closes it too. The header holds the title and a close button; the body scrolls between
15
+ * it and an optional pinned `footer`.
16
+ */
17
+ export function Drawer({ title, children, footer, size = "md", onClose, closeLabel = "Close", ...rest }) {
18
+ const dialog = useRef(null);
19
+ const titleId = useId();
20
+ const closed = useModal(dialog, onClose);
21
+ return (_jsxs("dialog", { ...rest, ref: dialog, "aria-labelledby": titleId, onClose: closed, onClick: (event) => event.target === dialog.current && dialog.current?.close(), className: cx(PANEL, SIZE[size]), children: [_jsxs("header", { className: "flex shrink-0 items-center justify-between gap-3 border-b border-slate-200 px-5.5 py-3.5", children: [_jsx(SectionHeading, { id: titleId, children: title }), _jsx(IconButton, { label: closeLabel, onClick: () => dialog.current?.close(), children: _jsx(Icon, { children: _jsx("svg", { viewBox: "0 0 20 20", fill: "currentColor", children: _jsx("path", { d: "M5.28 4.22a.75.75 0 0 0-1.06 1.06L8.94 10l-4.72 4.72a.75.75 0 1 0 1.06 1.06L10 11.06l4.72 4.72a.75.75 0 1 0 1.06-1.06L11.06 10l4.72-4.72a.75.75 0 0 0-1.06-1.06L10 8.94 5.28 4.22Z" }) }) }) })] }), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto px-5.5 py-4", children: children }), footer && (_jsx("footer", { className: "flex shrink-0 justify-end gap-2 border-t border-slate-200 px-5.5 py-3.5", children: footer }))] }));
22
+ }
@@ -0,0 +1,18 @@
1
+ import { type ReactElement, type ReactNode } from "react";
2
+ import type { Passthrough } from "./props.js";
3
+ /**
4
+ * What a list or a page shows when there is nothing in it yet: a centred `title`, the `children`
5
+ * as the explanation under it (a `Text as="p"`, then `Steps` for a "get started" list), and
6
+ * `actions` (buttons, links) in a centred row at the bottom. A named `<section>`. For one muted
7
+ * line inside a card, use `Text as="p" color="muted"`. `level` is the title's heading level (default 2): one below the
8
+ * page's own heading, so the empty state inside a `SectionHeading` section is a 3.
9
+ */
10
+ export declare function EmptyState({ title, level, children, actions, ...rest }: {
11
+ /** The heading, centred: what is missing ("No projects yet"). It names the section. */
12
+ title: ReactNode;
13
+ /** The title's heading level, 2 to 6. It does not change how the title looks. */
14
+ level?: 2 | 3 | 4 | 5 | 6 | undefined;
15
+ children?: ReactNode;
16
+ /** Buttons or links in a centred row at the bottom. */
17
+ actions?: ReactNode;
18
+ } & Passthrough): ReactElement;
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId } from "react";
3
+ import { SectionHeading } from "./typography.js";
4
+ /**
5
+ * What a list or a page shows when there is nothing in it yet: a centred `title`, the `children`
6
+ * as the explanation under it (a `Text as="p"`, then `Steps` for a "get started" list), and
7
+ * `actions` (buttons, links) in a centred row at the bottom. A named `<section>`. For one muted
8
+ * line inside a card, use `Text as="p" color="muted"`. `level` is the title's heading level (default 2): one below the
9
+ * page's own heading, so the empty state inside a `SectionHeading` section is a 3.
10
+ */
11
+ export function EmptyState({ title, level = 2, children, actions, ...rest }) {
12
+ const titleId = useId();
13
+ return (_jsxs("section", { ...rest, "aria-labelledby": titleId, className: "mx-auto flex w-full max-w-xl flex-col items-center gap-4 py-8 text-center", children: [_jsx(SectionHeading, { as: `h${level}`, id: titleId, children: title }), children != null && (_jsx("div", { className: "flex w-full flex-col items-center gap-5", children: children })), actions != null && _jsx("div", { className: "flex flex-wrap justify-center gap-2", children: actions })] }));
14
+ }
@@ -0,0 +1,21 @@
1
+ import type { ReactElement, ReactNode } from "react";
2
+ import type { Passthrough } from "./props.js";
3
+ /** The line under a control or a row: an error (`role="alert"`) replaces the hint; neither draws nothing. */
4
+ export declare function fieldNote(hint: ReactNode, error: ReactNode): ReactElement | null;
5
+ /**
6
+ * A label above a control, with a hint or an error line under it. An error replaces the hint.
7
+ * Inside a `FormGrid`, `span="full"` runs the field across both columns.
8
+ */
9
+ export declare function Field({ label, htmlFor, hint, error, span, children, ...rest }: {
10
+ /** The text above the control. */
11
+ label: ReactNode;
12
+ /** The id of the control inside; the label's htmlFor. */
13
+ htmlFor: string;
14
+ /** A muted line under the control that says more. */
15
+ hint?: ReactNode;
16
+ /** Replaces the hint when set. */
17
+ error?: ReactNode;
18
+ /** `full` spans every column of a `FormGrid`; unset takes one. It does nothing outside a `FormGrid`. */
19
+ span?: "full" | undefined;
20
+ children: ReactNode;
21
+ } & Passthrough): ReactElement;
package/dist/Field.js ADDED
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from "./cx.js";
3
+ /** The line under a control or a row: an error (`role="alert"`) replaces the hint; neither draws nothing. */
4
+ export function fieldNote(hint, error) {
5
+ if (error) {
6
+ return (_jsx("p", { role: "alert", className: "text-xs text-red-700", children: error }));
7
+ }
8
+ if (hint) {
9
+ return _jsx("p", { className: "text-xs text-slate-500", children: hint });
10
+ }
11
+ return null;
12
+ }
13
+ /**
14
+ * A label above a control, with a hint or an error line under it. An error replaces the hint.
15
+ * Inside a `FormGrid`, `span="full"` runs the field across both columns.
16
+ */
17
+ export function Field({ label, htmlFor, hint, error, span, children, ...rest }) {
18
+ return (_jsxs("div", { ...rest, className: cx("flex flex-col gap-1.5", span === "full" && "col-span-full"), children: [_jsx("label", { htmlFor: htmlFor, className: "text-sm font-medium text-slate-700", children: label }), children, fieldNote(hint, error)] }));
19
+ }
@@ -0,0 +1,28 @@
1
+ import { type ReactElement } from "react";
2
+ import type { Passthrough } from "./props.js";
3
+ export type FieldDialogProps = {
4
+ /** The dialog's heading and accessible name. */
5
+ title: string;
6
+ /** A muted line under the title. */
7
+ description?: string;
8
+ /** The visible label above the field. */
9
+ label: string;
10
+ /** The field's accessible name, when it says more than the visible label. */
11
+ inputLabel: string;
12
+ /** The field's placeholder. */
13
+ placeholder: string;
14
+ /** A muted line under the field; `emptyReason` takes its place while the field is empty. */
15
+ hint: string;
16
+ /** Said when the field is empty and the submit button is blocked. */
17
+ emptyReason: string;
18
+ /** The submit button's text. */
19
+ submit: string;
20
+ /** What the submit button says while `onSubmit` runs. */
21
+ busyLabel: string;
22
+ /** Gets the trimmed value. Resolving closes the dialog; a rejection shows its message in it. */
23
+ onSubmit: (value: string) => Promise<unknown>;
24
+ /** Called when the dialog closes: Cancel, Esc, the backdrop, or a resolved `onSubmit`. */
25
+ onClose: () => void;
26
+ } & Passthrough;
27
+ /** A `Dialog` with one text field and Cancel / submit buttons. Render it only while open. */
28
+ export declare function FieldDialog({ title, description, label, inputLabel, placeholder, hint, emptyReason, submit, busyLabel, onSubmit, onClose, ...rest }: FieldDialogProps): ReactElement;
@@ -0,0 +1,34 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId, useState } from "react";
3
+ import { Button } from "./Button.js";
4
+ import { Dialog } from "./Dialog.js";
5
+ import { Field } from "./Field.js";
6
+ import { Input } from "./Input.js";
7
+ /** The field's value and its in-flight state; `send` is the form's submit handler. */
8
+ function useFieldSubmit(onSubmit) {
9
+ const [value, setValue] = useState("");
10
+ const [busy, setBusy] = useState(false);
11
+ const [error, setError] = useState(null);
12
+ const empty = value.trim().length === 0;
13
+ const send = (event) => {
14
+ event.preventDefault();
15
+ if (empty || busy)
16
+ return;
17
+ // Close the <dialog> itself, so it animates out before `onClose` unmounts it.
18
+ const dialog = event.currentTarget.closest("dialog");
19
+ setError(null);
20
+ setBusy(true);
21
+ void onSubmit(value.trim()).then(() => dialog?.close(), (e) => {
22
+ setBusy(false);
23
+ setError(e instanceof Error ? e.message : String(e));
24
+ });
25
+ };
26
+ return { value, setValue, busy, error, empty, send };
27
+ }
28
+ /** A `Dialog` with one text field and Cancel / submit buttons. Render it only while open. */
29
+ export function FieldDialog({ title, description, label, inputLabel, placeholder, hint, emptyReason, submit, busyLabel, onSubmit, onClose, ...rest }) {
30
+ const id = useId();
31
+ const formId = `${id}-form`;
32
+ const { value, setValue, busy, error, empty, send } = useFieldSubmit(onSubmit);
33
+ return (_jsx(Dialog, { ...rest, title: title, description: description, onClose: onClose, footer: _jsxs(_Fragment, { children: [_jsx(Button, { variant: "ghost", onClick: (event) => event.currentTarget.closest("dialog")?.close(), children: "Cancel" }), _jsx(Button, { type: "submit", form: formId, disabledReason: empty ? emptyReason : undefined, busy: busy, busyLabel: busyLabel, children: submit })] }), children: _jsx("form", { id: formId, onSubmit: send, children: _jsx(Field, { label: label, htmlFor: id, hint: empty ? emptyReason : hint, error: error ?? undefined, children: _jsx(Input, { autoFocus: true, id: id, "aria-label": inputLabel, placeholder: placeholder, value: value, onChange: (e) => setValue(e.target.value), autoComplete: "off" }) }) }) }));
34
+ }
@@ -0,0 +1,17 @@
1
+ import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from "react";
2
+ import type { Unstyled } from "./props.js";
3
+ import type { Size } from "./size.js";
4
+ /**
5
+ * A dashed drop zone around a file input. The input covers the whole zone, invisible, so a click
6
+ * opens the file dialog and a file dragged onto it is taken by the browser as it is by any file
7
+ * input. `children` is the prompt; show the chosen file's name there from `onChange`. The label also shows
8
+ * the keyboard focus ring, the input being invisible. Every prop (`accept`, `multiple`, `name`, `disabled`, `onChange`) goes on the input.
9
+ */
10
+ export declare function FileDrop({ size, children, ...rest }: {
11
+ /**
12
+ * `sm` is a compact chip or tile: dense padding; `lg` is one step roomier than `md`.
13
+ * @default md
14
+ */
15
+ size?: Size;
16
+ children: ReactNode;
17
+ } & Unstyled<Omit<ComponentPropsWithoutRef<"input">, "type" | "children" | "size" | "width">>): ReactElement;
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from "./cx.js";
3
+ const PAD = { sm: "px-3 py-2", md: "px-4 py-6", lg: "px-5 py-8" };
4
+ /**
5
+ * A dashed drop zone around a file input. The input covers the whole zone, invisible, so a click
6
+ * opens the file dialog and a file dragged onto it is taken by the browser as it is by any file
7
+ * input. `children` is the prompt; show the chosen file's name there from `onChange`. The label also shows
8
+ * the keyboard focus ring, the input being invisible. Every prop (`accept`, `multiple`, `name`, `disabled`, `onChange`) goes on the input.
9
+ */
10
+ export function FileDrop({ size = "md", children, ...rest }) {
11
+ return (_jsxs("label", { className: cx("relative flex items-center justify-center rounded-lg border border-dashed border-slate-300 text-center text-sm text-slate-500 transition hover:border-violet-400 has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-violet-600", PAD[size], rest.disabled && "opacity-50"), children: [_jsx("input", { type: "file", className: "absolute inset-0 h-full w-full cursor-pointer rounded-lg opacity-0 disabled:cursor-not-allowed", ...rest }), children] }));
12
+ }
@@ -0,0 +1,7 @@
1
+ import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from "react";
2
+ import type { Unstyled } from "./props.js";
3
+ /** A labelled filter section: a small uppercase legend over a wrapped row of chips (e.g. `Button variant="outline" pressed`). Other props go on the fieldset. */
4
+ export declare function FilterGroup({ legend, children, ...rest }: {
5
+ /** The section's name, a small uppercase legend over the chips. */
6
+ legend: ReactNode;
7
+ } & Unstyled<ComponentPropsWithoutRef<"fieldset">>): ReactElement;
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Overline } from "./typography.js";
3
+ /** A labelled filter section: a small uppercase legend over a wrapped row of chips (e.g. `Button variant="outline" pressed`). Other props go on the fieldset. */
4
+ export function FilterGroup({ legend, children, ...rest }) {
5
+ return (_jsxs("fieldset", { ...rest, className: "min-w-0", children: [_jsx(Overline, { as: "legend", children: legend }), _jsx("div", { className: "mt-2 flex flex-wrap gap-1.5", children: children })] }));
6
+ }
@@ -0,0 +1,42 @@
1
+ import type { ReactElement, ReactNode } from "react";
2
+ import type { Passthrough } from "./props.js";
3
+ /** Relative widths in a `FormRow`: a field of `span` 2 is twice as wide as one of `span` 1. */
4
+ export type FormRowSpan = 1 | 2 | 3 | 4;
5
+ /**
6
+ * Two columns of `Field`s from `md`, one below it. A `Field` takes one column; `span="full"` runs
7
+ * it across both, and a `FormRow` is always full width.
8
+ */
9
+ export declare function FormGrid({ children, ...rest }: {
10
+ children: ReactNode;
11
+ } & Passthrough): ReactElement;
12
+ /**
13
+ * An inline-edit row: several controls side by side with proportional widths (`FormRowField`
14
+ * `span` 1 to 4), an optional `action` after them (a save button), and one `hint` or `error` line
15
+ * under the whole row. Every control sits on the bottom edge, so the action lines up with them
16
+ * whatever the labels do. Under `md` the fields and the action stack.
17
+ *
18
+ * The row owns the note, so the fields beside each other keep their height: a `FormRowField` has
19
+ * no `hint` or `error` of its own.
20
+ */
21
+ export declare function FormRow({ action, hint, error, children, ...rest }: {
22
+ /** A button or two after the last field. It keeps its own width. */
23
+ action?: ReactNode;
24
+ /** One muted line under the whole row. */
25
+ hint?: ReactNode;
26
+ /** Replaces the hint when set, as `Field`'s does. */
27
+ error?: ReactNode;
28
+ children: ReactNode;
29
+ } & Passthrough): ReactElement;
30
+ /** A compact label over one control, for a `FormRow`. `span` is its share of the row (1 default). */
31
+ export declare function FormRowField({ label, htmlFor, span, children, ...rest }: {
32
+ /** The small label above the control. */
33
+ label: ReactNode;
34
+ /** The id of the control inside; the label's htmlFor. */
35
+ htmlFor: string;
36
+ /**
37
+ * The field's share of the row, 1 to 4: a `span` of 2 is twice as wide as a 1.
38
+ * @default 1
39
+ */
40
+ span?: FormRowSpan | undefined;
41
+ children: ReactNode;
42
+ } & Passthrough): ReactElement;
@@ -0,0 +1,33 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from "./cx.js";
3
+ import { fieldNote } from "./Field.js";
4
+ const SPAN = {
5
+ 1: "md:flex-1",
6
+ 2: "md:flex-2",
7
+ 3: "md:flex-3",
8
+ 4: "md:flex-4",
9
+ };
10
+ /**
11
+ * Two columns of `Field`s from `md`, one below it. A `Field` takes one column; `span="full"` runs
12
+ * it across both, and a `FormRow` is always full width.
13
+ */
14
+ export function FormGrid({ children, ...rest }) {
15
+ return (_jsx("div", { ...rest, className: "grid grid-cols-1 gap-4 md:grid-cols-2", children: children }));
16
+ }
17
+ /**
18
+ * An inline-edit row: several controls side by side with proportional widths (`FormRowField`
19
+ * `span` 1 to 4), an optional `action` after them (a save button), and one `hint` or `error` line
20
+ * under the whole row. Every control sits on the bottom edge, so the action lines up with them
21
+ * whatever the labels do. Under `md` the fields and the action stack.
22
+ *
23
+ * The row owns the note, so the fields beside each other keep their height: a `FormRowField` has
24
+ * no `hint` or `error` of its own.
25
+ */
26
+ export function FormRow({ action, hint, error, children, ...rest }) {
27
+ const note = fieldNote(hint, error);
28
+ return (_jsxs("div", { ...rest, className: "col-span-full flex flex-col gap-1.5", children: [_jsxs("div", { className: "flex flex-col gap-3 md:flex-row md:items-end", children: [children, action && _jsx("div", { className: "flex items-center gap-2 md:shrink-0", children: action })] }), note] }));
29
+ }
30
+ /** A compact label over one control, for a `FormRow`. `span` is its share of the row (1 default). */
31
+ export function FormRowField({ label, htmlFor, span = 1, children, ...rest }) {
32
+ return (_jsxs("div", { ...rest, className: cx("flex min-w-0 flex-col gap-1", SPAN[span]), children: [_jsx("label", { htmlFor: htmlFor, className: "text-xs font-medium text-slate-700", children: label }), children] }));
33
+ }
package/dist/Icon.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ import type { ReactElement, ReactNode } from "react";
2
+ import type { Passthrough } from "./props.js";
3
+ import type { Size } from "./size.js";
4
+ import { type TextTone } from "./typography.js";
5
+ /**
6
+ * One inline icon: an `<svg>` child sized to a fixed box (sm 14px, md 16px, lg 20px) and drawn in
7
+ * `currentColor`, so it takes the text colour around it unless a `tone` says otherwise. It is
8
+ * decorative (`aria-hidden`) by default; a `label` makes it an image with that name, for an icon
9
+ * that carries meaning no text beside it says.
10
+ */
11
+ export declare function Icon({ size, tone, label, children, ...rest }: Passthrough & {
12
+ /** The box: sm 14px, md 16px (the default), lg 20px. */
13
+ size?: Size;
14
+ /** A text tone, or `inherit` (the default) for the colour of the text around it. */
15
+ tone?: TextTone | "inherit";
16
+ /** Names the icon (`role="img"`). Without one the icon is hidden from assistive tech. */
17
+ label?: string;
18
+ /** The `<svg>`, drawn in `currentColor`; it fills the box. */
19
+ children: ReactNode;
20
+ }): ReactElement;
package/dist/Icon.js ADDED
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cx } from "./cx.js";
3
+ import { TEXT_TONE } from "./typography.js";
4
+ const BOX = { sm: "size-3.5", md: "size-4", lg: "size-5" };
5
+ /**
6
+ * One inline icon: an `<svg>` child sized to a fixed box (sm 14px, md 16px, lg 20px) and drawn in
7
+ * `currentColor`, so it takes the text colour around it unless a `tone` says otherwise. It is
8
+ * decorative (`aria-hidden`) by default; a `label` makes it an image with that name, for an icon
9
+ * that carries meaning no text beside it says.
10
+ */
11
+ export function Icon({ size = "md", tone = "inherit", label, children, ...rest }) {
12
+ return (_jsx("span", { ...rest, ...(label ? { role: "img", "aria-label": label } : { "aria-hidden": true }), className: cx("inline-flex shrink-0 [&>svg]:size-full", BOX[size], tone !== "inherit" && TEXT_TONE[tone]), children: children }));
13
+ }