atlas-react-kit 0.10.0 → 0.11.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 (100) hide show
  1. package/COMPONENTS.md +50 -8
  2. package/DESIGN_RULES.md +12 -0
  3. package/README.md +2 -2
  4. package/dist/accordion.d.ts +8 -1
  5. package/dist/accordion.js +8 -3
  6. package/dist/alert.js +1 -1
  7. package/dist/avatar.d.ts +5 -1
  8. package/dist/avatar.js +7 -2
  9. package/dist/badge.js +1 -1
  10. package/dist/breadcrumb.js +3 -2
  11. package/dist/button.js +6 -6
  12. package/dist/calendar.js +19 -12
  13. package/dist/card.d.ts +1 -1
  14. package/dist/checkbox.js +2 -2
  15. package/dist/color-legend.d.ts +6 -4
  16. package/dist/color-legend.js +7 -4
  17. package/dist/combobox.d.ts +7 -2
  18. package/dist/combobox.js +35 -25
  19. package/dist/date-picker.d.ts +7 -13
  20. package/dist/date-picker.js +14 -20
  21. package/dist/date-time-picker.d.ts +5 -13
  22. package/dist/date-time-picker.js +26 -30
  23. package/dist/dialog.d.ts +5 -1
  24. package/dist/dialog.js +4 -3
  25. package/dist/dropdown-menu.js +5 -4
  26. package/dist/empty-panel.d.ts +4 -3
  27. package/dist/empty-panel.js +3 -3
  28. package/dist/error-boundary.d.ts +5 -3
  29. package/dist/error-boundary.js +9 -4
  30. package/dist/field.d.ts +14 -1
  31. package/dist/field.js +16 -3
  32. package/dist/form/date-picker-field.d.ts +8 -2
  33. package/dist/form/date-picker-field.js +4 -4
  34. package/dist/form/date-time-picker-field.d.ts +8 -1
  35. package/dist/form/date-time-picker-field.js +3 -3
  36. package/dist/form/feedback-field.d.ts +2 -1
  37. package/dist/form/feedback-field.js +3 -2
  38. package/dist/form/field-error.d.ts +4 -1
  39. package/dist/form/field-error.js +3 -2
  40. package/dist/form/field-shell.d.ts +25 -1
  41. package/dist/form/field-shell.js +36 -3
  42. package/dist/form/markdown-field.d.ts +7 -1
  43. package/dist/form/markdown-field.js +3 -3
  44. package/dist/form/select-field.d.ts +7 -1
  45. package/dist/form/select-field.js +3 -3
  46. package/dist/form/text-field.d.ts +5 -1
  47. package/dist/form/text-field.js +6 -3
  48. package/dist/info-tip.d.ts +2 -2
  49. package/dist/info-tip.js +34 -16
  50. package/dist/internal/form-control.d.ts +5 -0
  51. package/dist/internal/form-control.js +7 -0
  52. package/dist/internal/listbox-navigation.d.ts +32 -0
  53. package/dist/internal/listbox-navigation.js +38 -0
  54. package/dist/internal/listbox-option.d.ts +25 -0
  55. package/dist/internal/listbox-option.js +23 -0
  56. package/dist/internal/math.d.ts +5 -0
  57. package/dist/internal/math.js +7 -0
  58. package/dist/internal/overlay-styles.d.ts +28 -0
  59. package/dist/internal/overlay-styles.js +28 -0
  60. package/dist/internal/styles.d.ts +27 -0
  61. package/dist/internal/styles.js +27 -0
  62. package/dist/internal/tabs-styles.d.ts +10 -1
  63. package/dist/internal/tabs-styles.js +27 -5
  64. package/dist/lazy-calendar.js +9 -1
  65. package/dist/markdown.js +10 -4
  66. package/dist/meter.d.ts +3 -2
  67. package/dist/meter.js +6 -3
  68. package/dist/pagination.d.ts +8 -1
  69. package/dist/pagination.js +15 -9
  70. package/dist/picker-shell.d.ts +59 -0
  71. package/dist/picker-shell.js +21 -0
  72. package/dist/popover.js +2 -1
  73. package/dist/progress.d.ts +7 -3
  74. package/dist/progress.js +10 -4
  75. package/dist/radio-group.js +2 -2
  76. package/dist/search-picker.d.ts +4 -6
  77. package/dist/search-picker.js +28 -33
  78. package/dist/select.js +12 -6
  79. package/dist/separator.d.ts +4 -0
  80. package/dist/separator.js +5 -1
  81. package/dist/slider.d.ts +8 -3
  82. package/dist/slider.js +7 -5
  83. package/dist/sortable-table-head.d.ts +3 -1
  84. package/dist/sortable-table-head.js +3 -2
  85. package/dist/stat-tile.d.ts +6 -4
  86. package/dist/stat-tile.js +6 -4
  87. package/dist/switch.js +2 -2
  88. package/dist/table-body-skeleton.d.ts +8 -2
  89. package/dist/table-body-skeleton.js +7 -3
  90. package/dist/table.d.ts +7 -1
  91. package/dist/table.js +6 -2
  92. package/dist/tabs.d.ts +13 -3
  93. package/dist/tabs.js +29 -10
  94. package/dist/theme-provider.d.ts +2 -1
  95. package/dist/theme-provider.js +1 -1
  96. package/dist/toggle.js +4 -4
  97. package/dist/tooltip.d.ts +7 -3
  98. package/dist/tooltip.js +17 -6
  99. package/package.json +1 -1
  100. package/theme.css +43 -50
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Classes shared by the kit's floating surfaces (Popover, DropdownMenu,
3
+ * Select, Dialog, Tooltip) and the option rows inside them. Components add
4
+ * these with `cn()` instead of retyping them, so the panels and rows look
5
+ * and move the same everywhere. Tailwind only generates class names it
6
+ * finds as literals, so every class appears here in full.
7
+ */
8
+ /** The panel: square corners, popover colors, shadow, and a hairline ring. */
9
+ export const overlaySurface = "rounded-none bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10";
10
+ /** Slide in from the trigger, on whichever side Radix placed the panel. */
11
+ export const overlaySideSlide = "data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2";
12
+ /**
13
+ * Fade on open and close. `data-open` and `data-closed` (theme.css) match
14
+ * Radix's `data-state="open"` and `"closed"`.
15
+ */
16
+ export const overlayFade = "duration-100 data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0";
17
+ /** Fade plus a slight zoom, for panels (the backdrop only fades). */
18
+ export const overlayFadeZoom = `${overlayFade} data-open:zoom-in-95 data-closed:zoom-out-95`;
19
+ /**
20
+ * Base of a menu or listbox row (SelectItem, DropdownMenuItem, Combobox and
21
+ * SearchPicker options). Each component adds its own right padding and
22
+ * indicator, plus the highlight from `optionState`.
23
+ */
24
+ export const optionRow = "relative flex w-full cursor-default items-center gap-2.5 rounded-none py-2 pl-3 text-left text-sm outline-hidden select-none";
25
+ /** Icons in a row: no pointer events, no shrinking, 14px unless sized. */
26
+ export const optionIcon = "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5";
27
+ /** Group label in a menu or listbox (DropdownMenuLabel, SelectLabel). */
28
+ export const menuLabel = "px-3 py-2 text-xs font-semibold tracking-wider text-muted-foreground uppercase";
@@ -50,3 +50,30 @@ export declare const optionState: {
50
50
  /** The chosen value in lists that track selection in state. */
51
51
  selected: string;
52
52
  };
53
+ /** `aria-disabled` (links and cells that cannot be `disabled`): dimmed, with no hover or click. */
54
+ export declare const ariaDisabledState = "aria-disabled:pointer-events-none aria-disabled:opacity-50";
55
+ /**
56
+ * Larger click target for a small choice control (checkbox, radio, switch):
57
+ * an invisible `::after` 12px wider each side and 8px taller each side. The
58
+ * control needs `relative`.
59
+ */
60
+ export declare const choiceHitArea = "after:absolute after:-inset-x-3 after:-inset-y-2";
61
+ /**
62
+ * 44×44px invisible `::after` centred on a small control (slider thumb,
63
+ * icon trigger), the WCAG 2.5.5 target size. The control needs `relative`.
64
+ */
65
+ export declare const hitArea44 = "after:absolute after:top-1/2 after:left-1/2 after:size-11 after:-translate-x-1/2 after:-translate-y-1/2";
66
+ /**
67
+ * Height and horizontal padding for text controls, shared by Button and
68
+ * Toggle so a toolbar of both lines up. Padding tightens on the icon side.
69
+ */
70
+ export declare const controlSize: {
71
+ default: string;
72
+ sm: string;
73
+ lg: string;
74
+ };
75
+ /**
76
+ * Uppercase muted label used on tiles, legends, and thin section chrome.
77
+ * Re-exported publicly from `atlas-react-kit/stat-tile`.
78
+ */
79
+ export declare const sectionLabel = "text-xs font-semibold tracking-widest text-muted-foreground uppercase";
@@ -50,3 +50,30 @@ export const optionState = {
50
50
  /** The chosen value in lists that track selection in state. */
51
51
  selected: "bg-selected text-selected-foreground",
52
52
  };
53
+ /** `aria-disabled` (links and cells that cannot be `disabled`): dimmed, with no hover or click. */
54
+ export const ariaDisabledState = "aria-disabled:pointer-events-none aria-disabled:opacity-50";
55
+ /**
56
+ * Larger click target for a small choice control (checkbox, radio, switch):
57
+ * an invisible `::after` 12px wider each side and 8px taller each side. The
58
+ * control needs `relative`.
59
+ */
60
+ export const choiceHitArea = "after:absolute after:-inset-x-3 after:-inset-y-2";
61
+ /**
62
+ * 44×44px invisible `::after` centred on a small control (slider thumb,
63
+ * icon trigger), the WCAG 2.5.5 target size. The control needs `relative`.
64
+ */
65
+ export const hitArea44 = "after:absolute after:top-1/2 after:left-1/2 after:size-11 after:-translate-x-1/2 after:-translate-y-1/2";
66
+ /**
67
+ * Height and horizontal padding for text controls, shared by Button and
68
+ * Toggle so a toolbar of both lines up. Padding tightens on the icon side.
69
+ */
70
+ export const controlSize = {
71
+ default: "h-10 px-6 has-data-[icon=inline-end]:pr-4 has-data-[icon=inline-start]:pl-4",
72
+ sm: "h-9 px-4 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
73
+ lg: "h-11 px-8 has-data-[icon=inline-end]:pr-5 has-data-[icon=inline-start]:pl-5",
74
+ };
75
+ /**
76
+ * Uppercase muted label used on tiles, legends, and thin section chrome.
77
+ * Re-exported publicly from `atlas-react-kit/stat-tile`.
78
+ */
79
+ export const sectionLabel = "text-xs font-semibold tracking-widest text-muted-foreground uppercase";
@@ -1,5 +1,12 @@
1
+ /**
2
+ * Tab row. `TabsList` and `TabsNav` pass `orientation`, which picks plain
3
+ * size classes so a consumer `className` (`h-9`) replaces them. Called
4
+ * without it (`auto`), the row reads the orientation from the `group/tabs`
5
+ * root instead, as it always has for apps building their own list.
6
+ */
1
7
  declare const tabsListVariants: (props?: ({
2
8
  variant?: "default" | "line" | null | undefined;
9
+ orientation?: "auto" | "horizontal" | "vertical" | null | undefined;
3
10
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
4
11
  /**
5
12
  * Horizontal scroller around a tab row. `p-1` leaves room for the active
@@ -8,10 +15,12 @@ declare const tabsListVariants: (props?: ({
8
15
  * on an overflow box without this height makes `scrollHeight` 4px taller
9
16
  * than `clientHeight` (48 vs 44) and a vertical scrollbar appears if
10
17
  * `overflow-y` is not hidden. Line variant adds `pb-2` for the underline
11
- * (`h-14` = 44px + 4px + 8px).
18
+ * (`h-14` = 44px + 4px + 8px). A block, so it fills its parent without
19
+ * `w-full`. `theme.css` hides the scrollbar and draws the edge fades.
12
20
  */
13
21
  declare const tabsScrollerVariants: (props?: ({
14
22
  variant?: "default" | "line" | null | undefined;
23
+ orientation?: "horizontal" | "vertical" | null | undefined;
15
24
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
16
25
  declare const tabsTriggerVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
17
26
  export { tabsListVariants, tabsScrollerVariants, tabsTriggerVariants };
@@ -1,14 +1,26 @@
1
1
  import { cva } from "class-variance-authority";
2
2
  import { disabledState, focusRing, selectedState } from "./styles.js";
3
- const tabsListVariants = cva('group/tabs-list inline-flex w-max min-w-full items-stretch justify-start p-0 text-muted-foreground group-data-[orientation="horizontal"]/tabs:h-11 group-data-[orientation="vertical"]/tabs:h-fit group-data-[orientation="vertical"]/tabs:flex-col', {
3
+ /**
4
+ * Tab row. `TabsList` and `TabsNav` pass `orientation`, which picks plain
5
+ * size classes so a consumer `className` (`h-9`) replaces them. Called
6
+ * without it (`auto`), the row reads the orientation from the `group/tabs`
7
+ * root instead, as it always has for apps building their own list.
8
+ */
9
+ const tabsListVariants = cva("group/tabs-list inline-flex w-max min-w-full items-stretch justify-start p-0 text-muted-foreground", {
4
10
  variants: {
5
11
  variant: {
6
12
  default: "bg-muted",
7
13
  line: "gap-1 bg-transparent",
8
14
  },
15
+ orientation: {
16
+ auto: 'group-data-[orientation="horizontal"]/tabs:h-11 group-data-[orientation="vertical"]/tabs:h-fit group-data-[orientation="vertical"]/tabs:flex-col',
17
+ horizontal: "h-11",
18
+ vertical: "h-fit flex-col",
19
+ },
9
20
  },
10
21
  defaultVariants: {
11
22
  variant: "default",
23
+ orientation: "auto",
12
24
  },
13
25
  });
14
26
  /**
@@ -18,17 +30,27 @@ const tabsListVariants = cva('group/tabs-list inline-flex w-max min-w-full items
18
30
  * on an overflow box without this height makes `scrollHeight` 4px taller
19
31
  * than `clientHeight` (48 vs 44) and a vertical scrollbar appears if
20
32
  * `overflow-y` is not hidden. Line variant adds `pb-2` for the underline
21
- * (`h-14` = 44px + 4px + 8px).
33
+ * (`h-14` = 44px + 4px + 8px). A block, so it fills its parent without
34
+ * `w-full`. `theme.css` hides the scrollbar and draws the edge fades.
22
35
  */
23
- const tabsScrollerVariants = cva("relative box-border w-full min-w-0 max-w-full overflow-x-auto overflow-y-hidden overscroll-x-contain p-1 [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden", {
36
+ const tabsScrollerVariants = cva("relative box-border min-w-0 max-w-full overflow-x-auto overflow-y-hidden overscroll-x-contain p-1", {
24
37
  variants: {
25
38
  variant: {
26
- default: 'group-data-[orientation="horizontal"]/tabs:h-13',
27
- line: 'group-data-[orientation="horizontal"]/tabs:h-14 pb-2',
39
+ default: "",
40
+ line: "pb-2",
41
+ },
42
+ orientation: {
43
+ horizontal: "",
44
+ vertical: "",
28
45
  },
29
46
  },
47
+ compoundVariants: [
48
+ { variant: "default", orientation: "horizontal", className: "h-13" },
49
+ { variant: "line", orientation: "horizontal", className: "h-14" },
50
+ ],
30
51
  defaultVariants: {
31
52
  variant: "default",
53
+ orientation: "horizontal",
32
54
  },
33
55
  });
34
56
  const tabsTriggerVariants = cva([
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { Spinner } from "./spinner.js";
5
+ import { cn } from "./utils.js";
5
6
  /**
6
7
  * Calendar for the picker popovers, fetched on first use so a page that only
7
8
  * shows the trigger does not download Day Picker. Not a public entry: apps
@@ -13,6 +14,13 @@ export function preloadCalendar() {
13
14
  void loadCalendar();
14
15
  }
15
16
  const LazyCalendarInner = React.lazy(() => loadCalendar().then((module) => ({ default: module.Calendar })));
17
+ /**
18
+ * While loading, a placeholder the size of a one-month calendar, measured in
19
+ * the calendar's own `--cell-size` (7 columns plus `p-3`; about 9 cells
20
+ * tall), so the popover does not jump. It takes the calendar's `className`,
21
+ * so a `--cell-size` override sizes both.
22
+ */
23
+ const fallbackClassName = "flex h-[calc(var(--cell-size)*9)] w-[calc(var(--cell-size)*7+--spacing(6))] items-center justify-center [--cell-size:--spacing(8)]";
16
24
  export function LazyCalendar(props) {
17
- return (_jsx(React.Suspense, { fallback: _jsx("div", { className: "flex h-72 w-64 items-center justify-center", children: _jsx(Spinner, { "aria-label": "Loading calendar" }) }), children: _jsx(LazyCalendarInner, { ...props }) }));
25
+ return (_jsx(React.Suspense, { fallback: _jsx("div", { className: cn(fallbackClassName, props.className), children: _jsx(Spinner, { "aria-label": "Loading calendar" }) }), children: _jsx(LazyCalendarInner, { ...props }) }));
18
26
  }
package/dist/markdown.js CHANGED
@@ -13,8 +13,11 @@ export function safeMarkdownUrl(url) {
13
13
  const value = url.trim();
14
14
  if (!value)
15
15
  return "";
16
- if (value.startsWith("#") || value.startsWith("/"))
16
+ if (value.startsWith("#"))
17
17
  return value;
18
+ // `//host` and `/\host` are protocol-relative: they leave the site.
19
+ if (value.startsWith("/"))
20
+ return /^\/[/\\]/.test(value) ? "" : value;
18
21
  try {
19
22
  const parsed = new URL(value);
20
23
  return SAFE_PROTOCOLS.has(parsed.protocol) ? value : "";
@@ -45,9 +48,12 @@ const markdownComponents = {
45
48
  pre: ({ children }) => (_jsx("pre", { className: "overflow-x-auto bg-muted p-3 text-xs", children: children })),
46
49
  hr: () => _jsx("hr", { className: "border-border" }),
47
50
  table: ({ children }) => (_jsx("div", { className: "overflow-x-auto", children: _jsx("table", { className: "w-full text-left text-sm", children: children }) })),
48
- th: ({ children }) => (_jsx("th", { className: "border-b border-input py-1.5 pr-3 font-medium", children: children })),
49
- td: ({ children }) => (_jsx("td", { className: "border-b border-border py-1.5 pr-3", children: children })),
50
- img: ({ src, alt }) => (_jsx("img", { src: src, alt: alt ?? "", className: "max-w-full" })),
51
+ // remark-gfm sets column alignment as an inline text-align style.
52
+ th: ({ children, style }) => (_jsx("th", { style: style, className: "border-b border-input py-1.5 pr-3 font-medium", children: children })),
53
+ td: ({ children, style }) => (_jsx("td", { style: style, className: "border-b border-border py-1.5 pr-3", children: children })),
54
+ img: ({ src, alt }) =>
55
+ // src already passed through safeMarkdownUrl; an unsafe one is now empty.
56
+ src ? (_jsx("img", { src: src, alt: alt ?? "", className: "max-w-full" })) : alt ? (_jsx("span", { children: alt })) : null,
51
57
  };
52
58
  /**
53
59
  * GitHub-flavoured Markdown, token-styled and sanitized. No Next, no raw
package/dist/meter.d.ts CHANGED
@@ -1,9 +1,10 @@
1
+ import type * as React from "react";
1
2
  export type MeterMarker = {
2
3
  /** Same scale as `value` / `min` / `max` (not a 0–1 fraction). */
3
4
  position: number;
4
5
  className?: string;
5
6
  };
6
- export type MeterProps = {
7
+ export type MeterProps = Omit<React.ComponentProps<"div">, "children" | "role" | "aria-label"> & {
7
8
  value: number;
8
9
  min?: number;
9
10
  max: number;
@@ -18,5 +19,5 @@ export type MeterProps = {
18
19
  * Use `Progress` for a plain 0–100 job bar. The app chooses fill color and
19
20
  * marker positions (80% of a cap, 1.0× typical, and so on).
20
21
  */
21
- declare function Meter({ value, min, max, fillClassName, markers, "aria-label": ariaLabel, className, }: MeterProps): import("react").JSX.Element;
22
+ declare function Meter({ value, min, max, fillClassName, markers, "aria-label": ariaLabel, className, ...props }: MeterProps): React.JSX.Element;
22
23
  export { Meter };
package/dist/meter.js CHANGED
@@ -1,17 +1,20 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { clamp } from "./internal/math.js";
2
3
  import { cn } from "./utils.js";
3
4
  /**
4
5
  * Track, fill, and optional ticks for a current value against a scale.
5
6
  * Use `Progress` for a plain 0–100 job bar. The app chooses fill color and
6
7
  * marker positions (80% of a cap, 1.0× typical, and so on).
7
8
  */
8
- function Meter({ value, min = 0, max, fillClassName = "bg-primary", markers, "aria-label": ariaLabel, className, }) {
9
+ function Meter({ value, min = 0, max, fillClassName = "bg-primary", markers, "aria-label": ariaLabel, className, ...props }) {
9
10
  const span = max - min;
11
+ // A NaN reading (missing data upstream) draws and announces as empty.
12
+ const current = Number.isFinite(value) ? value : min;
10
13
  const toPercent = (amount) => {
11
14
  if (span === 0)
12
15
  return 0;
13
- return Math.min(100, Math.max(0, ((amount - min) / span) * 100));
16
+ return clamp(((amount - min) / span) * 100, 0, 100);
14
17
  };
15
- return (_jsxs("div", { role: "meter", "data-slot": "meter", "aria-valuemin": min, "aria-valuemax": max, "aria-valuenow": Math.min(max, Math.max(min, value)), "aria-label": ariaLabel, className: cn("relative h-1.5 w-full overflow-hidden bg-muted", className), children: [_jsx("div", { "data-slot": "meter-fill", className: cn("h-full", fillClassName), style: { width: `${toPercent(value)}%` } }), markers?.map((marker) => (_jsx("div", { "aria-hidden": true, "data-slot": "meter-marker", className: cn("absolute inset-y-0 w-px bg-foreground/40", marker.className), style: { left: `${toPercent(marker.position)}%` } }, marker.position)))] }));
18
+ return (_jsxs("div", { ...props, role: "meter", "data-slot": "meter", "aria-valuemin": min, "aria-valuemax": max, "aria-valuenow": clamp(current, min, max), "aria-label": ariaLabel, className: cn("relative h-1.5 w-full overflow-hidden bg-muted", className), children: [_jsx("div", { "data-slot": "meter-fill", className: cn("h-full", fillClassName), style: { width: `${toPercent(current)}%` } }), markers?.map((marker) => (_jsx("div", { "aria-hidden": true, "data-slot": "meter-marker", className: cn("absolute inset-y-0 w-px bg-foreground/40", marker.className), style: { left: `${toPercent(marker.position)}%` } }, marker.position)))] }));
16
19
  }
17
20
  export { Meter };
@@ -2,6 +2,8 @@ import * as React from "react";
2
2
  /**
3
3
  * Page links for long lists and tables. Links are plain anchors so the page
4
4
  * lives in the URL; pass `onClick` with `href` for client-side routing.
5
+ * The nav is a plain flex row: place it with `className` (`justify-center`,
6
+ * `justify-end`, `mx-auto`) to suit the layout.
5
7
  */
6
8
  declare function Pagination({ className, ...props }: React.ComponentProps<"nav">): React.JSX.Element;
7
9
  declare function PaginationContent({ className, ...props }: React.ComponentProps<"ul">): React.JSX.Element;
@@ -9,9 +11,14 @@ declare function PaginationItem({ ...props }: React.ComponentProps<"li">): React
9
11
  type PaginationLinkProps = {
10
12
  isActive?: boolean;
11
13
  size?: "default" | "icon";
14
+ /**
15
+ * No page to go to, such as Previous on the first page. Renders without
16
+ * `href`, announced as a disabled link and dimmed.
17
+ */
18
+ disabled?: boolean;
12
19
  } & React.ComponentProps<"a">;
13
20
  /** The current page gets `aria-current="page"` and the selected wash. */
14
- declare function PaginationLink({ className, isActive, size, ...props }: PaginationLinkProps): React.JSX.Element;
21
+ declare function PaginationLink({ className, isActive, size, disabled, href, ...props }: PaginationLinkProps): React.JSX.Element;
15
22
  declare function PaginationPrevious({ className, children, ...props }: React.ComponentProps<typeof PaginationLink>): React.JSX.Element;
16
23
  declare function PaginationNext({ className, children, ...props }: React.ComponentProps<typeof PaginationLink>): React.JSX.Element;
17
24
  declare function PaginationEllipsis({ className, ...props }: React.ComponentProps<"span">): React.JSX.Element;
@@ -2,14 +2,16 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon, } from "lucide-react";
4
4
  import { buttonVariants } from "./button.js";
5
- import { selectedState } from "./internal/styles.js";
5
+ import { ariaDisabledState, selectedState } from "./internal/styles.js";
6
6
  import { cn } from "./utils.js";
7
7
  /**
8
8
  * Page links for long lists and tables. Links are plain anchors so the page
9
9
  * lives in the URL; pass `onClick` with `href` for client-side routing.
10
+ * The nav is a plain flex row: place it with `className` (`justify-center`,
11
+ * `justify-end`, `mx-auto`) to suit the layout.
10
12
  */
11
13
  function Pagination({ className, ...props }) {
12
- return (_jsx("nav", { "aria-label": "Pagination", "data-slot": "pagination", className: cn("mx-auto flex w-full justify-center", className), ...props }));
14
+ return (_jsx("nav", { "aria-label": "Pagination", "data-slot": "pagination", className: cn("flex", className), ...props }));
13
15
  }
14
16
  function PaginationContent({ className, ...props }) {
15
17
  return (_jsx("ul", { "data-slot": "pagination-content", className: cn("flex flex-row items-center gap-1", className), ...props }));
@@ -18,17 +20,21 @@ function PaginationItem({ ...props }) {
18
20
  return _jsx("li", { "data-slot": "pagination-item", ...props });
19
21
  }
20
22
  /** The current page gets `aria-current="page"` and the selected wash. */
21
- function PaginationLink({ className, isActive, size = "icon", ...props }) {
22
- return (_jsx("a", { "aria-current": isActive ? "page" : undefined, "data-slot": "pagination-link", "data-active": isActive, className: cn(buttonVariants({
23
+ function PaginationLink({ className, isActive, size = "icon", disabled, href, ...props }) {
24
+ return (_jsx("a", { href: disabled ? undefined : href, role: disabled ? "link" : undefined, "aria-disabled": disabled || undefined, "aria-current": isActive ? "page" : undefined, "data-slot": "pagination-link", "data-active": isActive, className: cn(buttonVariants({
23
25
  variant: isActive ? "outline" : "ghost",
24
26
  size: size === "icon" ? "icon-sm" : "sm",
25
- }), selectedState.current, "font-medium tracking-normal normal-case tabular-nums", className), ...props }));
27
+ }), selectedState.current, ariaDisabledState, "font-medium tracking-normal normal-case tabular-nums", className), ...props }));
26
28
  }
27
- function PaginationPrevious({ className, children = "Previous", ...props }) {
28
- return (_jsxs(PaginationLink, { "aria-label": "Go to previous page", size: "default", className: cn("gap-1 px-2.5 sm:pl-2", className), ...props, children: [_jsx(ChevronLeftIcon, { "data-icon": "inline-start" }), _jsx("span", { className: "hidden sm:block", children: children })] }));
29
+ function PaginationPrevious({ className, children, ...props }) {
30
+ return (_jsxs(PaginationLink
31
+ // Custom (e.g. translated) text names the link itself (WCAG 2.5.3).
32
+ , { "aria-label": children == null ? "Go to previous page" : undefined, size: "default", className: cn("gap-1 px-2.5 sm:pl-2", className), ...props, children: [_jsx(ChevronLeftIcon, { "data-icon": "inline-start" }), _jsx("span", { className: "sr-only sm:not-sr-only", children: children ?? "Previous" })] }));
29
33
  }
30
- function PaginationNext({ className, children = "Next", ...props }) {
31
- return (_jsxs(PaginationLink, { "aria-label": "Go to next page", size: "default", className: cn("gap-1 px-2.5 sm:pr-2", className), ...props, children: [_jsx("span", { className: "hidden sm:block", children: children }), _jsx(ChevronRightIcon, { "data-icon": "inline-end" })] }));
34
+ function PaginationNext({ className, children, ...props }) {
35
+ return (_jsxs(PaginationLink
36
+ // Custom (e.g. translated) text names the link itself (WCAG 2.5.3).
37
+ , { "aria-label": children == null ? "Go to next page" : undefined, size: "default", className: cn("gap-1 px-2.5 sm:pr-2", className), ...props, children: [_jsx("span", { className: "sr-only sm:not-sr-only", children: children ?? "Next" }), _jsx(ChevronRightIcon, { "data-icon": "inline-end" })] }));
32
38
  }
33
39
  function PaginationEllipsis({ className, ...props }) {
34
40
  return (_jsxs("span", { "aria-hidden": true, "data-slot": "pagination-ellipsis", className: cn("flex size-9 items-center justify-center text-muted-foreground", className), ...props, children: [_jsx(MoreHorizontalIcon, { className: "size-4" }), _jsx("span", { className: "sr-only", children: "More pages" })] }));
@@ -0,0 +1,59 @@
1
+ import * as React from "react";
2
+ import type { PropsBase } from "react-day-picker";
3
+ import { PopoverContent } from "./popover.js";
4
+ /**
5
+ * Not a public entry. The trigger, popover, and Clear footer that
6
+ * `DatePickerButton` and `DateTimePickerButton` share.
7
+ */
8
+ /**
9
+ * Day Picker options for the picker's calendar: disabled matchers,
10
+ * `startMonth` / `endMonth`, `weekStartsOn`, `locale`, `className`, and so on.
11
+ * The picker owns the selection, focus, and month props.
12
+ */
13
+ export type PickerCalendarProps = Omit<PropsBase, "mode" | "required" | "autoFocus" | "defaultMonth" | "numberOfMonths">;
14
+ /** Props both pickers take for their trigger and popover. */
15
+ export type PickerTriggerProps = {
16
+ id?: string;
17
+ placeholder?: string;
18
+ disabled?: boolean;
19
+ /** Classes for the trigger button. */
20
+ className?: string;
21
+ /** Classes for the popover content, e.g. a width such as `w-80`. */
22
+ contentClassName?: string;
23
+ "aria-invalid"?: boolean;
24
+ /**
25
+ * Forwarded to the trigger. To name it from a visible label while still
26
+ * reading the picked value, list the label's id and then `${id}-value`
27
+ * (the trigger text). A `<label for>` alone names the button by the label
28
+ * and drops the value; it can stay for click-to-open.
29
+ */
30
+ "aria-labelledby"?: string;
31
+ /** Forwarded to the trigger, e.g. the id of a hint or error text. */
32
+ "aria-describedby"?: string;
33
+ /** Popover side. Defaults to `bottom`; Radix still flips on collision. */
34
+ side?: React.ComponentProps<typeof PopoverContent>["side"];
35
+ /** Month shown when the popover opens; falls back to the selected date. */
36
+ defaultMonth?: Date;
37
+ /** Day Picker options passed through to the calendar. */
38
+ calendarProps?: PickerCalendarProps;
39
+ };
40
+ type PickerShellProps = Omit<PickerTriggerProps, "placeholder" | "defaultMonth" | "calendarProps"> & {
41
+ /** Muted trigger text while nothing is picked. */
42
+ empty: boolean;
43
+ /** Trigger text: the formatted value or the placeholder. */
44
+ label: string;
45
+ /** Accessible name of the popover dialog. */
46
+ dialogLabel: string;
47
+ open: boolean;
48
+ onOpenChange: (open: boolean) => void;
49
+ onInteractOutside?: React.ComponentProps<typeof PopoverContent>["onInteractOutside"];
50
+ children: React.ReactNode;
51
+ };
52
+ /** Form-control trigger plus the viewport-clamped calendar popover. */
53
+ export declare function PickerShell({ id, disabled, className, contentClassName, "aria-invalid": ariaInvalid, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, side, empty, label, dialogLabel, open, onOpenChange, onInteractOutside, children, }: PickerShellProps): React.JSX.Element;
54
+ /** Clear button row under the calendar. */
55
+ export declare function PickerClearFooter({ disabled, onClear, }: {
56
+ disabled: boolean;
57
+ onClear: () => void;
58
+ }): React.JSX.Element;
59
+ export {};
@@ -0,0 +1,21 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { CalendarIcon } from "lucide-react";
5
+ import { Button } from "./button.js";
6
+ import { PICKER_COLLISION_PADDING, pickerContentClassName, pickerTriggerClassName, pickerValueId, } from "./internal/form-control.js";
7
+ import { preloadCalendar } from "./lazy-calendar.js";
8
+ import { Popover, PopoverContent, PopoverTrigger } from "./popover.js";
9
+ import { cn } from "./utils.js";
10
+ /** Form-control trigger plus the viewport-clamped calendar popover. */
11
+ export function PickerShell({ id, disabled, className, contentClassName, "aria-invalid": ariaInvalid, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, side, empty, label, dialogLabel, open, onOpenChange, onInteractOutside, children, }) {
12
+ return (_jsxs(Popover, { open: open, onOpenChange: onOpenChange, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { id: id, type: "button", variant: "outline", disabled: disabled, "aria-invalid": ariaInvalid, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "data-empty": empty, className: cn(pickerTriggerClassName, className), onPointerEnter: preloadCalendar, onFocus: preloadCalendar, children: [_jsx(CalendarIcon, { "data-icon": "inline-start" }), _jsx("span", { "data-slot": "date-picker-value", id: id ? pickerValueId(id) : undefined, children: label })] }) }), _jsx(PopoverContent, { align: "start", side: side, collisionPadding: PICKER_COLLISION_PADDING, "aria-label": dialogLabel,
13
+ // The calendar's `autoFocus` puts focus on the selected day (or
14
+ // today) once it loads, as the APG date picker dialog does; Radix
15
+ // focusing the popover first would leave it on the container.
16
+ onOpenAutoFocus: (event) => event.preventDefault(), onInteractOutside: onInteractOutside, className: cn("w-auto min-w-0", pickerContentClassName, contentClassName), children: children })] }));
17
+ }
18
+ /** Clear button row under the calendar. */
19
+ export function PickerClearFooter({ disabled, onClear, }) {
20
+ return (_jsx("div", { "data-slot": "date-picker-footer", className: "flex justify-end border-t p-2", children: _jsx(Button, { type: "button", variant: "ghost", size: "sm", disabled: disabled, onClick: onClear, children: "Clear" }) }));
21
+ }
package/dist/popover.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { Popover as PopoverPrimitive } from "radix-ui";
5
+ import { overlayFadeZoom, overlaySideSlide, overlaySurface, } from "./internal/overlay-styles.js";
5
6
  import { cn } from "./utils.js";
6
7
  function Popover({ ...props }) {
7
8
  return _jsx(PopoverPrimitive.Root, { "data-slot": "popover", ...props });
@@ -10,7 +11,7 @@ function PopoverTrigger({ ...props }) {
10
11
  return _jsx(PopoverPrimitive.Trigger, { "data-slot": "popover-trigger", ...props });
11
12
  }
12
13
  function PopoverContent({ className, align = "center", sideOffset = 4, collisionPadding = 8, ...props }) {
13
- return (_jsx(PopoverPrimitive.Portal, { children: _jsx(PopoverPrimitive.Content, { "data-slot": "popover-content", align: align, sideOffset: sideOffset, collisionPadding: collisionPadding, className: cn("z-50 flex max-w-(--radix-popover-content-available-width) origin-(--radix-popover-content-transform-origin) flex-col gap-4 rounded-none bg-popover p-4 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", className), ...props }) }));
14
+ return (_jsx(PopoverPrimitive.Portal, { children: _jsx(PopoverPrimitive.Content, { "data-slot": "popover-content", align: align, sideOffset: sideOffset, collisionPadding: collisionPadding, className: cn(overlaySurface, overlaySideSlide, overlayFadeZoom, "z-50 flex max-w-(--radix-popover-content-available-width) origin-(--radix-popover-content-transform-origin) flex-col gap-4 p-4 text-sm outline-hidden", className), ...props }) }));
14
15
  }
15
16
  function PopoverAnchor({ ...props }) {
16
17
  return _jsx(PopoverPrimitive.Anchor, { "data-slot": "popover-anchor", ...props });
@@ -1,8 +1,12 @@
1
1
  import * as React from "react";
2
2
  import { Progress as ProgressPrimitive } from "radix-ui";
3
3
  /**
4
- * Determinate progress (uploads, multi-step jobs). `value` is 0–100; omit
5
- * it for an indeterminate state and pair the bar with a visible label.
4
+ * Determinate progress (uploads, multi-step jobs). `value` runs from 0 to
5
+ * `max` (default 100) and is clamped to that range; omit it (or pass a
6
+ * non-number) for an indeterminate state and pair the bar with a visible
7
+ * label. `indicatorClassName` styles the fill (default `bg-primary`).
6
8
  */
7
- declare function Progress({ className, value, ...props }: React.ComponentProps<typeof ProgressPrimitive.Root>): React.JSX.Element;
9
+ declare function Progress({ className, indicatorClassName, value, max, ...props }: React.ComponentProps<typeof ProgressPrimitive.Root> & {
10
+ indicatorClassName?: string;
11
+ }): React.JSX.Element;
8
12
  export { Progress };
package/dist/progress.js CHANGED
@@ -2,12 +2,18 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { Progress as ProgressPrimitive } from "radix-ui";
5
+ import { clamp } from "./internal/math.js";
5
6
  import { cn } from "./utils.js";
6
7
  /**
7
- * Determinate progress (uploads, multi-step jobs). `value` is 0–100; omit
8
- * it for an indeterminate state and pair the bar with a visible label.
8
+ * Determinate progress (uploads, multi-step jobs). `value` runs from 0 to
9
+ * `max` (default 100) and is clamped to that range; omit it (or pass a
10
+ * non-number) for an indeterminate state and pair the bar with a visible
11
+ * label. `indicatorClassName` styles the fill (default `bg-primary`).
9
12
  */
10
- function Progress({ className, value, ...props }) {
11
- return (_jsx(ProgressPrimitive.Root, { "data-slot": "progress", value: value, className: cn("relative h-2 w-full overflow-hidden rounded-none bg-primary/15", className), ...props, children: _jsx(ProgressPrimitive.Indicator, { "data-slot": "progress-indicator", className: "h-full w-full flex-1 bg-primary transition-transform", style: { transform: `translateX(-${100 - (value ?? 0)}%)` } }) }));
13
+ function Progress({ className, indicatorClassName, value, max = 100, ...props }) {
14
+ const scale = Number.isFinite(max) && max > 0 ? max : 100;
15
+ const current = value != null && Number.isFinite(value) ? clamp(value, 0, scale) : null;
16
+ const filled = current == null ? 0 : (current / scale) * 100;
17
+ return (_jsx(ProgressPrimitive.Root, { "data-slot": "progress", value: current, max: scale, className: cn("relative h-2 w-full overflow-hidden rounded-none bg-primary/15", className), ...props, children: _jsx(ProgressPrimitive.Indicator, { "data-slot": "progress-indicator", className: cn("h-full w-full flex-1 bg-primary transition-transform", indicatorClassName), style: { transform: `translateX(-${100 - filled}%)` } }) }));
12
18
  }
13
19
  export { Progress };
@@ -2,7 +2,7 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { RadioGroup as RadioGroupPrimitive } from "radix-ui";
5
- import { disabledState, focusRing, invalidState } from "./internal/styles.js";
5
+ import { choiceHitArea, disabledState, focusRing, invalidState, } from "./internal/styles.js";
6
6
  import { cn } from "./utils.js";
7
7
  /**
8
8
  * One choice from a short, visible list. Arrow keys move the selection.
@@ -16,6 +16,6 @@ function RadioGroup({ className, ...props }) {
16
16
  * differently from square checkboxes.
17
17
  */
18
18
  function RadioGroupItem({ className, ...props }) {
19
- return (_jsx(RadioGroupPrimitive.Item, { "data-slot": "radio-group-item", className: cn(focusRing, invalidState, disabledState, "peer relative flex aspect-square size-4.5 shrink-0 items-center justify-center rounded-full border border-input bg-transparent shadow-xs transition-shadow after:absolute after:-inset-x-3 after:-inset-y-2 data-checked:border-primary", className), ...props, children: _jsx(RadioGroupPrimitive.Indicator, { "data-slot": "radio-group-indicator", className: "flex items-center justify-center", children: _jsx("span", { className: "size-2 rounded-full bg-primary" }) }) }));
19
+ return (_jsx(RadioGroupPrimitive.Item, { "data-slot": "radio-group-item", className: cn(focusRing, invalidState, disabledState, choiceHitArea, "peer relative flex aspect-square size-4.5 shrink-0 items-center justify-center rounded-full border border-input bg-transparent shadow-xs transition-shadow data-checked:border-primary", className), ...props, children: _jsx(RadioGroupPrimitive.Indicator, { "data-slot": "radio-group-indicator", className: "flex items-center justify-center", children: _jsx("span", { "data-slot": "radio-group-dot", className: "size-2 rounded-full bg-primary" }) }) }));
20
20
  }
21
21
  export { RadioGroup, RadioGroupItem };
@@ -1,8 +1,6 @@
1
1
  import * as React from "react";
2
- /**
3
- * Case-fold and strip combining marks so "nino" matches "Niño".
4
- */
5
- export declare function foldSearchText(value: string): string;
2
+ import { foldSearchText } from "./internal/listbox-navigation.js";
3
+ export { foldSearchText };
6
4
  /**
7
5
  * Default SearchPicker filter: the folded label includes the folded query.
8
6
  */
@@ -22,11 +20,11 @@ export type SearchPickerProps<T> = {
22
20
  className?: string;
23
21
  /** Classes for the listbox, e.g. its height. Defaults to `max-h-60`. */
24
22
  listClassName?: string;
25
- };
23
+ } & Omit<React.ComponentProps<"div">, "children" | "onSelect" | "defaultValue">;
26
24
  /**
27
25
  * Always-visible combobox: a labelled input and a listbox. Unlike Combobox,
28
26
  * the list is not in a popover. Keyboard: arrows, Home/End, PageUp/PageDown,
29
27
  * Enter, Escape.
30
28
  */
31
- declare function SearchPicker<T>({ items, getValue, getLabel, filter, value, onSelect, label, placeholder, emptyText, listLabel, disabled, className, listClassName, }: SearchPickerProps<T>): React.JSX.Element;
29
+ declare function SearchPicker<T>({ items, getValue, getLabel, filter, value, onSelect, label, placeholder, emptyText, listLabel, disabled, className, listClassName, ...props }: SearchPickerProps<T>): React.JSX.Element;
32
30
  export { SearchPicker };