@hackcove/ui 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +65 -6
  2. package/dist/AppShell.d.ts +27 -2
  3. package/dist/AppShell.js +25 -2
  4. package/dist/Badge.d.ts +9 -0
  5. package/dist/Badge.js +10 -0
  6. package/dist/Breadcrumbs.d.ts +22 -3
  7. package/dist/Breadcrumbs.js +23 -2
  8. package/dist/Button.d.ts +18 -8
  9. package/dist/Button.js +54 -22
  10. package/dist/ButtonGroup.d.ts +14 -0
  11. package/dist/ButtonGroup.js +10 -0
  12. package/dist/ButtonLink.d.ts +20 -0
  13. package/dist/ButtonLink.js +18 -0
  14. package/dist/Callout.d.ts +10 -2
  15. package/dist/Callout.js +8 -3
  16. package/dist/Checkbox.d.ts +10 -4
  17. package/dist/Checkbox.js +8 -3
  18. package/dist/ColourSchemeToggle.d.ts +14 -3
  19. package/dist/ColourSchemeToggle.js +13 -9
  20. package/dist/DateField.d.ts +15 -2
  21. package/dist/DateField.js +8 -4
  22. package/dist/DatePicker.d.ts +1 -1
  23. package/dist/DatePicker.js +3 -3
  24. package/dist/Drawer.d.ts +29 -0
  25. package/dist/Drawer.js +26 -0
  26. package/dist/FileDrop.d.ts +10 -0
  27. package/dist/FileDrop.js +11 -0
  28. package/dist/Input.d.ts +3 -2
  29. package/dist/Input.js +5 -4
  30. package/dist/KeyValueList.d.ts +11 -4
  31. package/dist/KeyValueList.js +14 -5
  32. package/dist/ListRow.d.ts +36 -0
  33. package/dist/ListRow.js +31 -0
  34. package/dist/NavTabs.d.ts +15 -4
  35. package/dist/NavTabs.js +13 -2
  36. package/dist/PageFade.d.ts +25 -0
  37. package/dist/PageFade.js +39 -0
  38. package/dist/ProgressBar.d.ts +26 -0
  39. package/dist/ProgressBar.js +32 -0
  40. package/dist/SegmentedControl.d.ts +11 -0
  41. package/dist/SegmentedControl.js +22 -0
  42. package/dist/Select.d.ts +3 -2
  43. package/dist/Select.js +4 -4
  44. package/dist/SplitBar.d.ts +21 -0
  45. package/dist/SplitBar.js +15 -0
  46. package/dist/StatTile.d.ts +16 -0
  47. package/dist/StatTile.js +21 -0
  48. package/dist/Table.d.ts +48 -0
  49. package/dist/Table.js +42 -0
  50. package/dist/Tabs.d.ts +13 -0
  51. package/dist/Tabs.js +23 -0
  52. package/dist/TextLink.d.ts +19 -4
  53. package/dist/TextLink.js +19 -4
  54. package/dist/Textarea.d.ts +3 -2
  55. package/dist/Textarea.js +4 -4
  56. package/dist/Timeline.d.ts +21 -0
  57. package/dist/Timeline.js +19 -0
  58. package/dist/colourScheme.d.ts +11 -2
  59. package/dist/colourScheme.js +16 -3
  60. package/dist/control.d.ts +19 -2
  61. package/dist/control.js +25 -2
  62. package/dist/index.d.ts +22 -6
  63. package/dist/index.js +19 -4
  64. package/dist/link.d.ts +22 -0
  65. package/dist/link.js +11 -0
  66. package/dist/switcher.d.ts +66 -0
  67. package/dist/switcher.js +53 -0
  68. package/dist/ui.d.ts +25 -5
  69. package/dist/ui.js +51 -7
  70. package/package.json +1 -1
@@ -1,24 +1,28 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useSyncExternalStore } from "react";
3
- import { applyColourScheme, storeColourScheme } from "./colourScheme.js";
3
+ import { appliedColourScheme, applyColourScheme, storeColourScheme, } from "./colourScheme.js";
4
4
  import { IconButton } from "./IconButton.js";
5
5
  const sun = (_jsxs("svg", { viewBox: "0 0 24 24", className: "h-4 w-4", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", "aria-hidden": "true", children: [_jsx("circle", { cx: "12", cy: "12", r: "4" }), _jsx("path", { d: "M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" })] }));
6
6
  const moon = (_jsx("svg", { viewBox: "0 0 24 24", className: "h-4 w-4", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", "aria-hidden": "true", children: _jsx("path", { d: "M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z" }) }));
7
- // The `dark` class on <html> is the source of truth: the head script set it before React ran.
7
+ // The scheme's class on <html> is the source of truth: the head script or the server set it before React ran.
8
8
  function subscribe(onChange) {
9
9
  const observer = new MutationObserver(onChange);
10
10
  observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });
11
11
  return () => observer.disconnect();
12
12
  }
13
- const current = () => document.documentElement.classList.contains("dark") ? "dark" : "light";
14
- // What the server and the hydrating client both render; the real state follows right after.
15
- const server = () => "light";
16
- /** A sun/moon button that switches `<html>` between light and dark and remembers the choice. It shows the scheme you would switch to. Pair it with `colourSchemeScript` in `<head>`. */
17
- export function ColourSchemeToggle({ className, onChange, }) {
18
- const scheme = useSyncExternalStore(subscribe, current, server);
13
+ /**
14
+ * A sun/moon button that switches `<html>` between light and dark and remembers the choice. It shows the scheme you would switch to. Pair it with `colourSchemeScript` in `<head>`, unless you pass `schemes` or `initial`: the script knows only the `dark` class and the stored choice, so an app that sets the scheme itself leaves it out.
15
+ *
16
+ * An app whose light scheme has a class of its own passes `schemes` (`{ light: "paper", dark: "dark" }`);
17
+ * the toggle swaps those classes, and `schemeClass` gives the same one for the server to render on `<html>`.
18
+ * An app that knows the scheme on the server (from a cookie) passes it as `initial`, so the server
19
+ * markup, the hydrating client and the `<html>` class agree; persist a change from `onChange`.
20
+ */
21
+ export function ColourSchemeToggle({ className, onChange, schemes = { light: "", dark: "dark" }, initial, }) {
22
+ const scheme = useSyncExternalStore(subscribe, () => appliedColourScheme(document.documentElement, schemes), () => initial ?? "light");
19
23
  const next = scheme === "dark" ? "light" : "dark";
20
24
  return (_jsx(IconButton, { label: `Switch to ${next} theme`, className: className, onClick: () => {
21
- applyColourScheme(next);
25
+ applyColourScheme(next, document.documentElement, schemes);
22
26
  storeColourScheme(next);
23
27
  onChange?.(next);
24
28
  }, children: next === "light" ? sun : moon }));
@@ -14,6 +14,10 @@ export type DateFieldProps = {
14
14
  description?: ReactNode;
15
15
  /** Replaces the built-in error message (an impossible date, or one outside min/max). */
16
16
  errorMessage?: ReactNode;
17
+ /** An error from outside the field (a server's, say). Shown in place of the description, in red with `role="alert"`, and marks the field invalid. Like `Field`'s `error`. */
18
+ error?: ReactNode;
19
+ /** Marks the field invalid with no message of its own; pair it with `errorMessage`. */
20
+ isInvalid?: boolean;
17
21
  className?: string;
18
22
  } & Pick<AriaDateFieldProps<CalendarDate>, "value" | "defaultValue" | "onChange" | "minValue" | "maxValue" | "isDisabled" | "isRequired" | "name" | "aria-label">;
19
23
  /** What the segments show, as the day segment reports it to the field. */
@@ -35,15 +39,24 @@ export declare function frameClass({ isInvalid, isFocusWithin, isDisabled, }: {
35
39
  isFocusWithin: boolean;
36
40
  isDisabled: boolean;
37
41
  }): string;
42
+ /** 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. */
43
+ export declare function forcedInvalid({ impossible, isInvalid, error, }: {
44
+ impossible: string | null;
45
+ isInvalid: boolean | undefined;
46
+ error: ReactNode;
47
+ }): {
48
+ isInvalid?: true;
49
+ };
38
50
  /** The label, the description or the error line: the shared outside of both components. */
39
- export declare function Notes({ description, errorMessage, impossible, isInvalid, }: {
51
+ export declare function Notes({ description, errorMessage, error, impossible, isInvalid, }: {
40
52
  description: ReactNode;
41
53
  errorMessage: ReactNode;
54
+ error: ReactNode;
42
55
  impossible: string | null;
43
56
  isInvalid: boolean;
44
57
  }): ReactElement;
45
58
  export declare const labelClass = "text-sm font-medium text-slate-700";
46
59
  export declare const iconButtonClass = "inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-slate-500 transition hover:bg-slate-100 disabled:opacity-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-600";
47
60
  /** A segmented DD / MM / YYYY date input. Values are `CalendarDate`s; `name` posts it as `YYYY-MM-DD`. */
48
- export declare function DateField({ label, description, errorMessage, className, value, defaultValue, onChange, name, ...rest }: DateFieldProps): ReactElement;
61
+ export declare function DateField({ label, description, errorMessage, error, isInvalid: invalidProp, className, value, defaultValue, onChange, name, ...rest }: DateFieldProps): ReactElement;
49
62
  export {};
package/dist/DateField.js CHANGED
@@ -57,14 +57,18 @@ export function frameClass({ isInvalid, isFocusWithin, isDisabled, }) {
57
57
  tone = "border-violet-600 bg-white text-slate-900";
58
58
  return cx("flex w-full items-center rounded-lg border py-2 pl-2.5 text-sm transition", tone, isDisabled && "opacity-50");
59
59
  }
60
+ /** 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. */
61
+ export function forcedInvalid({ impossible, isInvalid, error, }) {
62
+ return impossible || isInvalid || error ? { isInvalid: true } : {};
63
+ }
60
64
  /** The label, the description or the error line: the shared outside of both components. */
61
- export function Notes({ description, errorMessage, impossible, isInvalid, }) {
62
- 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 }) => errorMessage ?? impossible ?? validationErrors.join(" ") })] }));
65
+ export function Notes({ description, errorMessage, error, impossible, isInvalid, }) {
66
+ 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(" "))) })] }));
63
67
  }
64
68
  export const labelClass = "text-sm font-medium text-slate-700";
65
69
  export const iconButtonClass = `inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-slate-500 transition hover:bg-slate-100 disabled:opacity-50 ${focusRing}`;
66
70
  /** A segmented DD / MM / YYYY date input. Values are `CalendarDate`s; `name` posts it as `YYYY-MM-DD`. */
67
- export function DateField({ label, description, errorMessage, className, value, defaultValue, onChange, name, ...rest }) {
71
+ export function DateField({ label, description, errorMessage, error, isInvalid: invalidProp, className, value, defaultValue, onChange, name, ...rest }) {
68
72
  const { inner, setInner, shown, impossible, onRead, validate, leaving, onBlurCapture, onLeft } = useShownDate({
69
73
  value,
70
74
  defaultValue,
@@ -73,5 +77,5 @@ export function DateField({ label, description, errorMessage, className, value,
73
77
  maxValue: rest.maxValue,
74
78
  isRequired: rest.isRequired,
75
79
  });
76
- return (_jsx(I18nProvider, { locale: LOCALE, children: _jsx(AriaDateField, { ...rest, value: inner, onChange: setInner, validate: validate, ...(impossible ? { isInvalid: true } : {}), isReadOnly: leaving, onBlur: onLeft, granularity: "day", shouldForceLeadingZeros: true, className: cx("flex flex-col gap-1.5", className), 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), "pr-2.5"), 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, impossible: impossible, isInvalid: isInvalid })] })) }) }));
80
+ 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: cx("flex flex-col gap-1.5", className), 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), "pr-2.5"), 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 })] })) }) }));
77
81
  }
@@ -5,4 +5,4 @@ export type DatePickerProps = DateFieldProps & {
5
5
  defaultOpen?: boolean;
6
6
  };
7
7
  /** A `DateField` with a calendar button that opens a month view in a popover. */
8
- export declare function DatePicker({ label, description, errorMessage, className, value, defaultValue, onChange, name, ...rest }: DatePickerProps): ReactElement;
8
+ export declare function DatePicker({ label, description, errorMessage, error, isInvalid: invalidProp, className, value, defaultValue, onChange, name, ...rest }: DatePickerProps): ReactElement;
@@ -3,7 +3,7 @@ import { useContext } from "react";
3
3
  import { Button, DatePicker as AriaDatePicker, DateFieldContext, DateInput, Group, I18nProvider, Label, } from "react-aria-components";
4
4
  import { cx } from "./cx.js";
5
5
  import { CalendarPopover, calendarIcon } from "./DateCalendar.js";
6
- import { FieldSegment, LOCALE, Notes, frameClass, iconButtonClass, labelClass, } from "./DateField.js";
6
+ import { FieldSegment, LOCALE, forcedInvalid, Notes, frameClass, iconButtonClass, labelClass, } from "./DateField.js";
7
7
  import { ShownInput, useShownDate } from "./shownDate.js";
8
8
  /**
9
9
  * The picker's input, read-only while `leaving`. Only the field: a read-only picker would also
@@ -14,7 +14,7 @@ function PickerInput({ leaving, onLeft, children, }) {
14
14
  return (_jsx(DateFieldContext.Provider, { value: { ...field, isReadOnly: leaving, onBlur: onLeft }, children: children }));
15
15
  }
16
16
  /** A `DateField` with a calendar button that opens a month view in a popover. */
17
- export function DatePicker({ label, description, errorMessage, className, value, defaultValue, onChange, name, ...rest }) {
17
+ export function DatePicker({ label, description, errorMessage, error, isInvalid: invalidProp, className, value, defaultValue, onChange, name, ...rest }) {
18
18
  const { inner, setInner, shown, impossible, onRead, validate, leaving, onBlurCapture, onLeft } = useShownDate({
19
19
  value,
20
20
  defaultValue,
@@ -23,5 +23,5 @@ export function DatePicker({ label, description, errorMessage, className, value,
23
23
  maxValue: rest.maxValue,
24
24
  isRequired: rest.isRequired,
25
25
  });
26
- return (_jsx(I18nProvider, { locale: LOCALE, children: _jsx(AriaDatePicker, { ...rest, value: inner, onChange: setInner, validate: validate, ...(impossible ? { isInvalid: true } : {}), granularity: "day", shouldForceLeadingZeros: true, className: cx("flex flex-col gap-1.5", className), children: ({ isInvalid }) => (_jsxs(_Fragment, { children: [label ? _jsx(Label, { className: labelClass, children: label }) : null, _jsxs(Group, { className: (state) => cx(frameClass(state), "gap-1 pr-1"), 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: iconButtonClass, children: calendarIcon })] }), _jsx(ShownInput, { name: name, shown: shown, isDisabled: rest.isDisabled }), _jsx(Notes, { description: description, errorMessage: errorMessage, impossible: impossible, isInvalid: isInvalid }), _jsx(CalendarPopover, {})] })) }) }));
26
+ return (_jsx(I18nProvider, { locale: LOCALE, children: _jsx(AriaDatePicker, { ...rest, value: inner, onChange: setInner, validate: validate, ...forcedInvalid({ impossible, isInvalid: invalidProp, error }), granularity: "day", shouldForceLeadingZeros: true, className: cx("flex flex-col gap-1.5", className), children: ({ isInvalid }) => (_jsxs(_Fragment, { children: [label ? _jsx(Label, { className: labelClass, children: label }) : null, _jsxs(Group, { className: (state) => cx(frameClass(state), "gap-1 pr-1"), 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: iconButtonClass, 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, {})] })) }) }));
27
27
  }
@@ -0,0 +1,29 @@
1
+ import { type ReactElement, type ReactNode } from "react";
2
+ declare const SIZE: {
3
+ readonly sm: "w-80";
4
+ readonly md: "w-110";
5
+ readonly lg: "w-160";
6
+ };
7
+ export type DrawerProps = {
8
+ /** The drawer's accessible name, shown as its heading. */
9
+ title: ReactNode;
10
+ children?: ReactNode;
11
+ /** Actions pinned under the scrolling body, right-aligned. */
12
+ footer?: ReactNode;
13
+ /** `md` (the default) holds a form or a record; `sm` is narrower and `lg` wider. */
14
+ size?: keyof typeof SIZE;
15
+ /** Called when the drawer closes: Esc, a click on the scrim or the close button. */
16
+ onClose: () => void;
17
+ /** The accessible name of the close button. */
18
+ closeLabel?: string;
19
+ className?: string;
20
+ };
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, className, }: DrawerProps): ReactElement;
29
+ export {};
package/dist/Drawer.js ADDED
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useLayoutEffect, useId, useRef } from "react";
3
+ import { cx } from "./cx.js";
4
+ import { IconButton } from "./IconButton.js";
5
+ import { ModalHeading } from "./typography.js";
6
+ 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 backdrop:bg-slate-900/35 open:flex transition-transform duration-200 starting:open:translate-x-full motion-reduce:transition-none";
7
+ const SIZE = { sm: "w-80", md: "w-110", lg: "w-160" };
8
+ /**
9
+ * A modal slide-over anchored to the right edge, on the browser's own `<dialog>`: it is opened
10
+ * with `showModal()` on mount, so render it only while it is open. The page behind goes inert, Esc
11
+ * closes it and focus stays inside; a click on the scrim (the `<dialog>` element itself, not its
12
+ * contents) closes it too. The header holds the title and a close button; the body scrolls between
13
+ * it and an optional pinned `footer`.
14
+ */
15
+ export function Drawer({ title, children, footer, size = "md", onClose, closeLabel = "Close", className, }) {
16
+ const dialog = useRef(null);
17
+ const titleId = useId();
18
+ // Closing on unmount (not just on Esc or the scrim) hands focus back to the opener when the
19
+ // parent unmounts the drawer, and keeps StrictMode's second mount from reopening an open dialog.
20
+ useLayoutEffect(() => {
21
+ const el = dialog.current;
22
+ el?.showModal();
23
+ return () => el?.close();
24
+ }, []);
25
+ return (_jsxs("dialog", { ref: dialog, "aria-labelledby": titleId, onClose: onClose, onClick: (event) => event.target === dialog.current && dialog.current?.close(), className: cx(PANEL, SIZE[size], className), 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(ModalHeading, { id: titleId, children: title }), _jsx(IconButton, { label: closeLabel, onClick: () => dialog.current?.close(), children: _jsx("svg", { viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": "true", className: "h-4 w-4", 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 }))] }));
26
+ }
@@ -0,0 +1,10 @@
1
+ import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from "react";
2
+ /**
3
+ * A dashed drop zone around a file input. The input covers the whole zone, invisible, so a click
4
+ * opens the file dialog and a file dragged onto it is taken by the browser as it is by any file
5
+ * input. `children` is the prompt; show the chosen file's name there from `onChange`. `className`
6
+ * goes on the label (which also shows the keyboard focus ring, the input being invisible), every other prop (`accept`, `multiple`, `name`, `disabled`, `onChange`) on the input.
7
+ */
8
+ export declare function FileDrop({ children, className, ...rest }: {
9
+ children: ReactNode;
10
+ } & Omit<ComponentPropsWithoutRef<"input">, "type" | "children" | "size" | "width">): ReactElement;
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from "./cx.js";
3
+ /**
4
+ * A dashed drop zone around a file input. The input covers the whole zone, invisible, so a click
5
+ * opens the file dialog and a file dragged onto it is taken by the browser as it is by any file
6
+ * input. `children` is the prompt; show the chosen file's name there from `onChange`. `className`
7
+ * goes on the label (which also shows the keyboard focus ring, the input being invisible), every other prop (`accept`, `multiple`, `name`, `disabled`, `onChange`) on the input.
8
+ */
9
+ export function FileDrop({ children, className, ...rest }) {
10
+ return (_jsxs("label", { className: cx("relative flex items-center justify-center rounded-lg border border-dashed border-slate-300 px-4 py-6 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", rest.disabled && "opacity-50", className), 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] }));
11
+ }
package/dist/Input.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from "react";
2
+ import { type ControlProps } from "./control.js";
2
3
  /** A text input. `icon` puts a leading icon inside the field (e.g. a search glass). */
3
- export declare function Input({ icon, className, ...rest }: {
4
+ export declare function Input({ icon, size, width, className, ...rest }: {
4
5
  icon?: ReactNode;
5
- } & ComponentPropsWithoutRef<"input">): ReactElement;
6
+ } & ControlProps & Omit<ComponentPropsWithoutRef<"input">, "size" | "width">): ReactElement;
package/dist/Input.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { controlClass } from "./control.js";
2
+ import { CONTROL_PAD, CONTROL_WIDTH, controlClass } from "./control.js";
3
3
  import { cx } from "./cx.js";
4
4
  /** A text input. `icon` puts a leading icon inside the field (e.g. a search glass). */
5
- export function Input({ icon, className, ...rest }) {
5
+ export function Input({ icon, size = "md", width = "full", className, ...rest }) {
6
6
  if (icon === undefined) {
7
- return _jsx("input", { ...rest, className: cx(controlClass, "px-3", className) });
7
+ return (_jsx("input", { ...rest, className: cx(controlClass(size, width), CONTROL_PAD[size].x, className) }));
8
8
  }
9
- return (_jsxs("div", { className: "relative", children: [_jsx("span", { "aria-hidden": "true", className: "pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-slate-400 [&>svg]:h-4 [&>svg]:w-4", children: icon }), _jsx("input", { ...rest, className: cx(controlClass, "pr-3 pl-9", className) })] }));
9
+ const small = size === "sm";
10
+ return (_jsxs("div", { className: cx("relative", width === "auto" && "inline-block", width !== "full" && CONTROL_WIDTH[width]), children: [_jsx("span", { "aria-hidden": "true", className: cx("pointer-events-none absolute top-1/2 -translate-y-1/2 text-slate-400", small ? "left-2 [&>svg]:h-3.5 [&>svg]:w-3.5" : "left-3 [&>svg]:h-4 [&>svg]:w-4"), children: icon }), _jsx("input", { ...rest, className: cx(controlClass(size, width === "auto" ? "auto" : "full"), CONTROL_PAD[size].icon, className) })] }));
10
11
  }
@@ -1,10 +1,17 @@
1
- import type { HTMLAttributes, ReactElement, ReactNode } from "react";
2
- /** A definition list of label/value rows: each label on the left, its value right-aligned. */
3
- export declare function KeyValueList({ className, children, ...rest }: {
1
+ import { type HTMLAttributes, type ReactElement, type ReactNode } from "react";
2
+ type KeyValueAlign = "end" | "start";
3
+ /**
4
+ * A definition list of label/value rows. By default each label is on the left and its value
5
+ * right-aligned. `align="start"` puts every value left-aligned in one column beside the labels,
6
+ * which are sized to the widest.
7
+ */
8
+ export declare function KeyValueList({ align, className, children, ...rest }: {
9
+ align?: KeyValueAlign;
4
10
  children: ReactNode;
5
11
  } & HTMLAttributes<HTMLDListElement>): ReactElement;
6
- /** One row of a KeyValueList: a label on the left, a right-aligned tabular value on the right. */
12
+ /** One row of a KeyValueList: a label on the left, a tabular value on the right (or beside it, in an `align="start"` list). */
7
13
  export declare function KeyValue({ label, children, className, ...rest }: {
8
14
  label: ReactNode;
9
15
  children: ReactNode;
10
16
  } & HTMLAttributes<HTMLDivElement>): ReactElement;
17
+ export {};
@@ -1,10 +1,19 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { createContext, useContext, } from "react";
2
3
  import { cx } from "./cx.js";
3
- /** A definition list of label/value rows: each label on the left, its value right-aligned. */
4
- export function KeyValueList({ className, children, ...rest }) {
5
- return (_jsx("dl", { className: cx("flex flex-col gap-1.5 text-sm", className), ...rest, children: children }));
4
+ const AlignContext = createContext("end");
5
+ /**
6
+ * A definition list of label/value rows. By default each label is on the left and its value
7
+ * right-aligned. `align="start"` puts every value left-aligned in one column beside the labels,
8
+ * which are sized to the widest.
9
+ */
10
+ export function KeyValueList({ align = "end", className, children, ...rest }) {
11
+ return (_jsx("dl", { className: cx(align === "start"
12
+ ? "grid grid-cols-[max-content_1fr] gap-x-3 gap-y-1.5"
13
+ : "flex flex-col gap-1.5", "text-sm", className), ...rest, children: _jsx(AlignContext.Provider, { value: align, children: children }) }));
6
14
  }
7
- /** One row of a KeyValueList: a label on the left, a right-aligned tabular value on the right. */
15
+ /** One row of a KeyValueList: a label on the left, a tabular value on the right (or beside it, in an `align="start"` list). */
8
16
  export function KeyValue({ label, children, className, ...rest }) {
9
- return (_jsxs("div", { className: cx("flex justify-between gap-3", className), ...rest, children: [_jsx("dt", { className: "text-slate-600", children: label }), _jsx("dd", { className: "text-right tabular-nums", children: children })] }));
17
+ const start = useContext(AlignContext) === "start";
18
+ return (_jsxs("div", { className: cx(start ? "col-span-2 grid grid-cols-subgrid" : "flex justify-between gap-3", className), ...rest, children: [_jsx("dt", { className: "text-slate-600", children: label }), _jsx("dd", { className: start ? "tabular-nums" : "text-right tabular-nums", children: children })] }));
10
19
  }
@@ -0,0 +1,36 @@
1
+ import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from "react";
2
+ import { type LinkRender } from "./link.js";
3
+ type Shared = {
4
+ /** Content at the far end of the row: a value, a count, a Pill. */
5
+ trailing?: ReactNode | undefined;
6
+ children: ReactNode;
7
+ };
8
+ type ListRowButtonProps = Shared & {
9
+ /**
10
+ * Makes the row a collapsible header: sets `aria-expanded` and shows a chevron that turns down
11
+ * when open. The caller owns the state and the section; pass `aria-controls` to name it.
12
+ */
13
+ expanded?: boolean | undefined;
14
+ href?: undefined;
15
+ render?: undefined;
16
+ } & Omit<ComponentPropsWithoutRef<"button">, "children">;
17
+ type ListRowLinkProps = Shared & {
18
+ /** Opens in a new tab: adds `target="_blank"` and `rel="noreferrer"`. */
19
+ external?: boolean | undefined;
20
+ expanded?: undefined;
21
+ } & Omit<ComponentPropsWithoutRef<"a">, "href" | "children"> & ({
22
+ href: string;
23
+ render?: LinkRender | undefined;
24
+ } | {
25
+ href?: string | undefined;
26
+ render: LinkRender;
27
+ });
28
+ export type ListRowProps = ListRowButtonProps | ListRowLinkProps;
29
+ /**
30
+ * A full-width row that is a `<button>` (or, with `href` or `render`, a link) with a hover fill, a
31
+ * left-aligned label and `trailing` content at the far end. With `expanded` it is the header of a
32
+ * collapsible section: `aria-expanded` and a chevron. Rows sit in a `Card flush`; lines between
33
+ * them are the caller's (`border-t border-slate-200/70` on each row after the first).
34
+ */
35
+ export declare function ListRow(props: ListRowProps): ReactElement;
36
+ export {};
@@ -0,0 +1,31 @@
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 ROW = `flex w-full items-center gap-3 px-4 py-3 text-left text-sm text-slate-900 hover:bg-slate-50 disabled:pointer-events-none disabled:opacity-50 ${focusRing} focus-visible:-outline-offset-2`;
5
+ function Chevron({ open }) {
6
+ return (_jsx("svg", { "aria-hidden": "true", viewBox: "0 0 20 20", fill: "currentColor", className: cx("h-4 w-4 shrink-0 text-slate-400", open && "rotate-90"), children: _jsx("path", { fillRule: "evenodd", d: "M7.2 4.8a.75.75 0 0 1 1.06 0l4.7 4.67a.75.75 0 0 1 0 1.06l-4.7 4.67a.75.75 0 1 1-1.06-1.06L11.37 10 7.2 5.86a.75.75 0 0 1 0-1.06Z", clipRule: "evenodd" }) }));
7
+ }
8
+ /**
9
+ * A full-width row that is a `<button>` (or, with `href` or `render`, a link) with a hover fill, a
10
+ * left-aligned label and `trailing` content at the far end. With `expanded` it is the header of a
11
+ * collapsible section: `aria-expanded` and a chevron. Rows sit in a `Card flush`; lines between
12
+ * them are the caller's (`border-t border-slate-200/70` on each row after the first).
13
+ */
14
+ export function ListRow(props) {
15
+ const { trailing, children, className } = props;
16
+ const label = _jsx("span", { className: "min-w-0 flex-1", children: children });
17
+ const end = trailing != null && (_jsx("span", { className: "shrink-0 tabular-nums text-slate-500", children: trailing }));
18
+ if (props.href !== undefined || props.render !== undefined) {
19
+ const { trailing: _t, expanded: _e, href, render, external, className: _c, ...rest } = props;
20
+ return renderLink({
21
+ ...rest,
22
+ href,
23
+ render,
24
+ external,
25
+ className: cx(ROW, className),
26
+ children: (_jsxs(_Fragment, { children: [label, end] })),
27
+ });
28
+ }
29
+ const { trailing: _t, expanded, href: _h, render: _r, className: _c, ...rest } = props;
30
+ return (_jsxs("button", { type: "button", ...rest, ...(expanded === undefined ? {} : { "aria-expanded": expanded }), className: cx(ROW, className), children: [expanded !== undefined && _jsx(Chevron, { open: expanded }), label, end] }));
31
+ }
package/dist/NavTabs.d.ts CHANGED
@@ -1,13 +1,24 @@
1
1
  import type { ReactElement, ReactNode } from "react";
2
- /** One pill link in a NavTabs row. */
2
+ import { type LinkRender } from "./link.js";
3
+ /**
4
+ * One pill link in a NavTabs row: a plain `<a href>`, or with `render` the app's own router
5
+ * `<Link>` (`render: (p) => <Link to="/stores" {...p} />`, which gets the tab's classes and
6
+ * `aria-current`; `href` is then not used). `external` opens in a new tab.
7
+ */
3
8
  export type NavTab = {
4
9
  label: ReactNode;
5
- href: string;
6
10
  active?: boolean;
7
- };
11
+ external?: boolean;
12
+ } & ({
13
+ href: string;
14
+ render?: LinkRender;
15
+ } | {
16
+ href?: string;
17
+ render: LinkRender;
18
+ });
8
19
  /** The classes of one tab, for a router's own <Link>. */
9
20
  export declare function navTabClass(active: boolean): string;
10
- /** A row of pill links where the active one is filled. Renders plain <a>; use navTabClass with a router's Link. */
21
+ /** A row of pill links where the active one is filled. Plain <a> by default; a tab's `render` draws a router's Link. */
11
22
  export declare function NavTabs({ items, label, className, }: {
12
23
  items: NavTab[];
13
24
  /** The accessible name of the nav landmark. */
package/dist/NavTabs.js CHANGED
@@ -1,11 +1,22 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { cx, focusRing } from "./cx.js";
3
+ import { renderLink } from "./link.js";
3
4
  const BASE = `rounded-lg px-3 py-1.5 transition ${focusRing}`;
4
5
  /** The classes of one tab, for a router's own <Link>. */
5
6
  export function navTabClass(active) {
6
7
  return cx(BASE, active ? "bg-slate-100 font-medium text-slate-900" : "text-slate-600 hover:bg-slate-100");
7
8
  }
8
- /** A row of pill links where the active one is filled. Renders plain <a>; use navTabClass with a router's Link. */
9
+ /** A row of pill links where the active one is filled. Plain <a> by default; a tab's `render` draws a router's Link. */
9
10
  export function NavTabs({ items, label, className, }) {
10
- return (_jsx("nav", { "aria-label": label, className: cx("flex flex-wrap gap-1 text-sm", className), children: items.map((item) => (_jsx("a", { href: item.href, className: navTabClass(!!item.active), "aria-current": item.active ? "page" : undefined, children: item.label }, item.href))) }));
11
+ return (_jsx("nav", { "aria-label": label, className: cx("flex flex-wrap gap-1 text-sm", className), children: items.map((item, index) => (_jsx(Tab, { item: item }, item.href ?? index))) }));
12
+ }
13
+ function Tab({ item }) {
14
+ return renderLink({
15
+ href: item.href,
16
+ render: item.render,
17
+ external: item.external,
18
+ className: navTabClass(!!item.active),
19
+ "aria-current": item.active ? "page" : undefined,
20
+ children: item.label,
21
+ });
11
22
  }
@@ -0,0 +1,25 @@
1
+ import type { HTMLAttributes, ReactElement, ReactNode } from "react";
2
+ /**
3
+ * The page's content area, named for the View Transitions API so a route change cross-fades it
4
+ * while the frame around it (header, footer) stays put. Wrap what changes between pages, once per
5
+ * page: two on one page need a different `name` each, or the browser skips the transition.
6
+ * The fade runs only when the navigation is wrapped in a view transition: `startPageTransition`,
7
+ * or a router's own option (TanStack Router `viewTransition`, React Router's `viewTransition` prop,
8
+ * Astro's `ClientRouter`). Otherwise, or where the browser has no View Transitions, it renders
9
+ * and behaves as a plain `<div>`. A `view-transition-name` makes the div a stacking context, so a
10
+ * 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
11
+ * transition of the rest of the page is the router's to quiet.
12
+ */
13
+ export declare function PageFade({ name, className, children, ...rest }: {
14
+ /** The view-transition name, a CSS identifier (letters, digits, `-`, `_`). */
15
+ name?: string;
16
+ children: ReactNode;
17
+ } & HTMLAttributes<HTMLDivElement>): ReactElement;
18
+ /**
19
+ * Run `update` inside a view transition, so a `PageFade` cross-fades. `update` is a synchronous
20
+ * state change, flushed at once as the browser needs the new DOM; for a router's navigation, which
21
+ * is async, use the router's own view-transition option instead. Where the browser has
22
+ * no View Transitions, or the user prefers reduced motion, `update` just runs. Call it from an event
23
+ * handler, not during render.
24
+ */
25
+ 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, className, children, ...rest }) {
22
+ return (_jsxs(_Fragment, { children: [_jsx("style", { children: css(name) }), _jsx("div", { "data-page-fade": name, className: className, ...rest, 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,26 @@
1
+ import type { ReactElement } from "react";
2
+ /** The colour of a bar's fill. */
3
+ export type BarTone = "accent" | "positive" | "warn" | "negative";
4
+ /** 6px, 8px or 12px tall. */
5
+ export type BarSize = "sm" | "md" | "lg";
6
+ export declare const BAR_FILL: Record<BarTone, string>;
7
+ export declare const BAR_TRACK: Record<BarSize, string>;
8
+ /** `part` as a percent of `whole`, held to 0–100; 0 when either is not positive. */
9
+ export declare function sharePercent(part: number, whole: number): number;
10
+ /** The share as a whole percent for a screen reader: never 0 or 100 unless it truly is. */
11
+ export declare function announcedPercent(part: number, whole: number): number;
12
+ /**
13
+ * A thin horizontal bar with a filled share, for progress toward a total. `value` is held to
14
+ * 0–`max`. `label` names what it measures, for a screen reader, which also hears the percentage.
15
+ */
16
+ export declare function ProgressBar({ value, max, label, tone, size, className, }: {
17
+ value: number;
18
+ /** Defaults to 100. */
19
+ max?: number;
20
+ label: string;
21
+ /** Defaults to "accent". */
22
+ tone?: BarTone;
23
+ /** Defaults to "sm". */
24
+ size?: BarSize;
25
+ className?: string;
26
+ }): 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 = "sm", className, }) {
30
+ const share = sharePercent(value, max);
31
+ return (_jsx("div", { 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], className), children: _jsx("div", { className: cx("h-full", BAR_FILL[tone]), style: { width: `${share}%` } }) }));
32
+ }
@@ -0,0 +1,11 @@
1
+ import type { ReactElement } from "react";
2
+ import { type SwitchItem, type SwitchProps } from "./switcher.js";
3
+ /** One option of a SegmentedControl. See `SwitchItem`. */
4
+ export type Segment = SwitchItem;
5
+ /**
6
+ * A joined pill group where one option is filled: a mode or a range chosen in place. `items` are
7
+ * `{ value, label }` buttons (`value` and `onChange` are the caller's state; the arrow keys, Home
8
+ * and End move the choice) or links (`href`, or `render` to draw the app's own router `<Link>`;
9
+ * `value` marks the current one, which gets `aria-current`). `label` names the group.
10
+ */
11
+ export declare function SegmentedControl(props: SwitchProps): ReactElement;
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cx } from "./cx.js";
3
+ import { Switcher } from "./switcher.js";
4
+ // The focus ring sits inside the option, since the group's rounded edge would clip an outer one.
5
+ const BASE = "border-l border-slate-300 px-3 py-1.5 transition first:rounded-l-full first:border-l-0 last:rounded-r-full focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-violet-600";
6
+ const ON = "bg-violet-600 font-medium text-violet-50";
7
+ const OFF = "bg-white text-slate-600 hover:bg-violet-50 hover:text-violet-700";
8
+ /**
9
+ * A joined pill group where one option is filled: a mode or a range chosen in place. `items` are
10
+ * `{ value, label }` buttons (`value` and `onChange` are the caller's state; the arrow keys, Home
11
+ * and End move the choice) or links (`href`, or `render` to draw the app's own router `<Link>`;
12
+ * `value` marks the current one, which gets `aria-current`). `label` names the group.
13
+ */
14
+ export function SegmentedControl(props) {
15
+ return (_jsx(Switcher, { ...props, parts: {
16
+ group: "radiogroup",
17
+ item: "radio",
18
+ vertical: true,
19
+ root: "inline-flex rounded-full border border-slate-300 text-sm",
20
+ option: (active) => cx(BASE, active ? ON : OFF),
21
+ } }));
22
+ }
package/dist/Select.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  import type { ComponentPropsWithoutRef, ReactElement } from "react";
2
- /** A native `<select>` styled like `Input`, keeping the browser's own arrow. */
3
- export declare function Select({ className, children, ...rest }: ComponentPropsWithoutRef<"select">): ReactElement;
2
+ import { type ControlProps } from "./control.js";
3
+ /** A native `<select>` styled like `Input`, keeping the browser's own arrow. Takes `size` and `width` too. */
4
+ export declare function Select({ size, width, className, children, ...rest }: ControlProps & Omit<ComponentPropsWithoutRef<"select">, "size" | "width">): ReactElement;