@hex-core/components 0.2.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -125,11 +125,21 @@ declare const RadioGroup: React$1.ForwardRefExoticComponent<Omit<RadioGroupPrimi
125
125
  /** A single radio option within a RadioGroup. */
126
126
  declare const RadioGroupItem: React$1.ForwardRefExoticComponent<Omit<RadioGroupPrimitive.RadioGroupItemProps & React$1.RefAttributes<HTMLButtonElement>, "ref"> & React$1.RefAttributes<HTMLButtonElement>>;
127
127
 
128
+ interface SliderProps extends React$1.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> {
129
+ /**
130
+ * Per-thumb accessible labels. When the slider has multiple thumbs, pass
131
+ * one entry per thumb (e.g. ["Minimum", "Maximum"]). For a single-thumb
132
+ * slider, the Root's `aria-label` / `aria-labelledby` is mirrored onto
133
+ * the thumb automatically — pass `thumbLabels` only when those defaults
134
+ * are insufficient.
135
+ */
136
+ thumbLabels?: string[];
137
+ }
128
138
  /**
129
139
  * A range input with one or more draggable thumbs.
130
140
  * Built on Radix UI Slider with keyboard controls (arrows, Home, End, PageUp/Down).
131
141
  */
132
- declare const Slider: React$1.ForwardRefExoticComponent<Omit<SliderPrimitive.SliderProps & React$1.RefAttributes<HTMLSpanElement>, "ref"> & React$1.RefAttributes<HTMLSpanElement>>;
142
+ declare const Slider: React$1.ForwardRefExoticComponent<SliderProps & React$1.RefAttributes<HTMLSpanElement>>;
133
143
 
134
144
  declare const toggleVariants: (props?: ({
135
145
  variant?: "default" | "outline" | null | undefined;
@@ -175,8 +185,17 @@ declare function Skeleton({ className, ...props }: React$1.HTMLAttributes<HTMLDi
175
185
  */
176
186
  declare const Progress: React$1.ForwardRefExoticComponent<Omit<ProgressPrimitive.ProgressProps & React$1.RefAttributes<HTMLDivElement>, "ref"> & React$1.RefAttributes<HTMLDivElement>>;
177
187
 
188
+ interface ScrollAreaProps extends React$1.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> {
189
+ /**
190
+ * tabIndex applied to the scroll viewport so keyboard users can scroll
191
+ * without a pointer. Defaults to `0` (focusable) — pass `-1` to skip the
192
+ * viewport in the tab order when ScrollArea wraps purely decorative or
193
+ * already-keyboard-reachable content.
194
+ */
195
+ viewportTabIndex?: number;
196
+ }
178
197
  /** A scrollable area with custom-styled scrollbars. Content must be explicitly sized. */
179
- declare const ScrollArea: React$1.ForwardRefExoticComponent<Omit<ScrollAreaPrimitive.ScrollAreaProps & React$1.RefAttributes<HTMLDivElement>, "ref"> & React$1.RefAttributes<HTMLDivElement>>;
198
+ declare const ScrollArea: React$1.ForwardRefExoticComponent<ScrollAreaProps & React$1.RefAttributes<HTMLDivElement>>;
180
199
  /** Styled scrollbar track + thumb. Rendered inside ScrollArea automatically. */
181
200
  declare const ScrollBar: React$1.ForwardRefExoticComponent<Omit<ScrollAreaPrimitive.ScrollAreaScrollbarProps & React$1.RefAttributes<HTMLDivElement>, "ref"> & React$1.RefAttributes<HTMLDivElement>>;
182
201
 
@@ -224,8 +243,22 @@ declare const DialogPortal: React$1.FC<DialogPrimitive.DialogPortalProps>;
224
243
  declare const DialogClose: React$1.ForwardRefExoticComponent<DialogPrimitive.DialogCloseProps & React$1.RefAttributes<HTMLButtonElement>>;
225
244
  /** Dimmed backdrop rendered behind the dialog content. */
226
245
  declare const DialogOverlay: React$1.ForwardRefExoticComponent<Omit<DialogPrimitive.DialogOverlayProps & React$1.RefAttributes<HTMLDivElement>, "ref"> & React$1.RefAttributes<HTMLDivElement>>;
246
+ interface DialogContentProps extends React$1.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {
247
+ /**
248
+ * When `true` (the default), DialogContent caps its height at viewport-2rem
249
+ * and renders children inside a padded inner scroll container. The Close
250
+ * button stays anchored to the (non-scrolling) outer panel so it remains
251
+ * visible even when the user scrolls long content.
252
+ *
253
+ * Pass `scrollable={false}` to opt out — useful when the consumer manages
254
+ * its own scroll surface (e.g. CommandDialog defers scroll to cmdk's
255
+ * internal CommandList).
256
+ */
257
+ scrollable?: boolean;
258
+ }
227
259
  /** The dialog content panel, centered on the overlay. Includes a close button by default. */
228
- declare const DialogContent: React$1.ForwardRefExoticComponent<Omit<DialogPrimitive.DialogContentProps & React$1.RefAttributes<HTMLDivElement>, "ref"> & React$1.RefAttributes<HTMLDivElement>>;
260
+ declare const DialogContent: React$1.ForwardRefExoticComponent<DialogContentProps & React$1.RefAttributes<HTMLDivElement>>;
261
+
229
262
  /**
230
263
  * Header container inside DialogContent; stacks title and description.
231
264
  * @returns A div wrapping title/description with vertical rhythm
@@ -511,7 +544,11 @@ declare const TableRow: React$1.ForwardRefExoticComponent<React$1.HTMLAttributes
511
544
  declare const TableHead: React$1.ForwardRefExoticComponent<React$1.ThHTMLAttributes<HTMLTableCellElement> & React$1.RefAttributes<HTMLTableCellElement>>;
512
545
  /** `<td>` with consistent padding. */
513
546
  declare const TableCell: React$1.ForwardRefExoticComponent<React$1.TdHTMLAttributes<HTMLTableCellElement> & React$1.RefAttributes<HTMLTableCellElement>>;
514
- /** `<caption>` positioned below the table. */
547
+ /**
548
+ * Visible `<caption>` rendered below the table. The parent `<Table>` sets
549
+ * `caption-bottom`, so the caption is announced first by screen readers when
550
+ * entering the table, then visually placed below the rows.
551
+ */
515
552
  declare const TableCaption: React$1.ForwardRefExoticComponent<React$1.HTMLAttributes<HTMLTableCaptionElement> & React$1.RefAttributes<HTMLTableCaptionElement>>;
516
553
 
517
554
  /**
@@ -523,13 +560,24 @@ declare const TableCaption: React$1.ForwardRefExoticComponent<React$1.HTMLAttrib
523
560
  interface DataTableProps<TData> {
524
561
  columns: ColumnDef<TData, unknown>[];
525
562
  data: TData[];
563
+ /**
564
+ * Visible caption rendered below the table. Announced by screen readers
565
+ * when the user enters the table. Provide either `caption` or `aria-label`.
566
+ */
567
+ caption?: React$1.ReactNode;
568
+ /**
569
+ * Accessible label for the table when no visible caption is shown.
570
+ * Forwarded as `aria-label` on the underlying `<table>` element. Kebab-case
571
+ * to match the canonical ARIA prop convention used elsewhere in Hex UI.
572
+ */
573
+ "aria-label"?: string;
526
574
  }
527
575
  /**
528
576
  * Render a data-driven table from TanStack column definitions.
529
- * @param props - Columns and data
577
+ * @param props - Columns, data, and optional accessible labelling (`caption` or `aria-label`)
530
578
  * @returns A styled Table rendered from the TanStack row model
531
579
  */
532
- declare function DataTable<TData>({ columns, data }: DataTableProps<TData>): react_jsx_runtime.JSX.Element;
580
+ declare function DataTable<TData>({ columns, data, caption, "aria-label": ariaLabel, }: DataTableProps<TData>): react_jsx_runtime.JSX.Element;
533
581
 
534
582
  /**
535
583
  * Root nav landmark for pagination controls.
@@ -690,14 +738,17 @@ declare const CommandGroup: React$1.ForwardRefExoticComponent<Omit<{
690
738
  value?: string;
691
739
  forceMount?: boolean;
692
740
  } & React$1.RefAttributes<HTMLDivElement>, "ref"> & React$1.RefAttributes<HTMLDivElement>>;
693
- /** Horizontal rule between groups. */
694
- declare const CommandSeparator: React$1.ForwardRefExoticComponent<Omit<Pick<Pick<React$1.DetailedHTMLProps<React$1.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof React$1.HTMLAttributes<HTMLDivElement>> & {
695
- ref?: React$1.Ref<HTMLDivElement>;
696
- } & {
697
- asChild?: boolean;
698
- }, "asChild" | "key" | keyof React$1.HTMLAttributes<HTMLDivElement>> & {
699
- alwaysRender?: boolean;
700
- } & React$1.RefAttributes<HTMLDivElement>, "ref"> & React$1.RefAttributes<HTMLDivElement>>;
741
+ /**
742
+ * Horizontal rule between groups. Renders as a presentational `<div>` (no role)
743
+ * so it can sit inside CommandList (role=listbox) without violating ARIA's
744
+ * required-children rule for listbox. The line is purely decorative — cmdk's
745
+ * built-in Separator hardcodes `role="separator"`, which axe rejects in this
746
+ * context, so we render the divider directly.
747
+ *
748
+ * The `data-cmdk-separator` attribute is preserved so existing CSS / test
749
+ * selectors that target cmdk's separator continue to match.
750
+ */
751
+ declare const CommandSeparator: React$1.ForwardRefExoticComponent<React$1.HTMLAttributes<HTMLDivElement> & React$1.RefAttributes<HTMLDivElement>>;
701
752
  /** Selectable item. onSelect fires on Enter or click. */
702
753
  declare const CommandItem: React$1.ForwardRefExoticComponent<Omit<{
703
754
  children?: React$1.ReactNode;
@@ -746,8 +797,10 @@ interface ComboboxProps {
746
797
  disabled?: boolean;
747
798
  /** Extra class names on the trigger button. */
748
799
  className?: string;
749
- /** Accessible label for the trigger (required when no adjacent visible <label>). */
800
+ /** Accessible label for the trigger (required when no adjacent visible label). */
750
801
  "aria-label"?: string;
802
+ /** Id of an external visible label that names this combobox. */
803
+ "aria-labelledby"?: string;
751
804
  }
752
805
  /**
753
806
  * Searchable select input built on Command + Popover.
@@ -756,7 +809,7 @@ interface ComboboxProps {
756
809
  * the trigger; the popover contains a CommandInput and filtered CommandList.
757
810
  * @returns A trigger button that opens a filtered option list.
758
811
  */
759
- declare function Combobox({ options, value, onChange, placeholder, searchPlaceholder, emptyText, disabled, className, "aria-label": ariaLabel, }: ComboboxProps): react_jsx_runtime.JSX.Element;
812
+ declare function Combobox({ options, value, onChange, placeholder, searchPlaceholder, emptyText, disabled, className, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, }: ComboboxProps): react_jsx_runtime.JSX.Element;
760
813
  declare namespace Combobox {
761
814
  var displayName: string;
762
815
  }
package/dist/index.js CHANGED
@@ -46,7 +46,9 @@ var buttonVariants = cva(
46
46
  secondary: [
47
47
  "bg-secondary text-secondary-foreground",
48
48
  "shadow-sm",
49
- "hover:bg-secondary/80 hover:shadow-md"
49
+ // Hover: shadow-elevation only — opacity-based bg shift would push apparent
50
+ // contrast below 3:1 vs --card during hover, regressing WCAG 1.4.11.
51
+ "hover:shadow-md"
50
52
  ].join(" "),
51
53
  ghost: "hover:bg-accent hover:text-accent-foreground",
52
54
  link: "text-primary underline-offset-4 hover:underline"
@@ -288,7 +290,9 @@ var badgeVariants = cva3(
288
290
  variants: {
289
291
  variant: {
290
292
  default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
291
- secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
293
+ // Hover stays at full --secondary fill — opacity-based shift would drop the
294
+ // composite contrast below 3:1 against --card on hover (WCAG 1.4.11).
295
+ secondary: "border-transparent bg-secondary text-secondary-foreground",
292
296
  destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
293
297
  outline: "text-foreground"
294
298
  }
@@ -488,30 +492,46 @@ RadioGroupItem.displayName = "RadioGroupItem";
488
492
  import * as SliderPrimitive from "@radix-ui/react-slider";
489
493
  import * as React10 from "react";
490
494
  import { jsx as jsx11, jsxs as jsxs4 } from "react/jsx-runtime";
491
- var Slider = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsxs4(
492
- SliderPrimitive.Root,
493
- {
494
- ref,
495
- className: cn("relative flex w-full touch-none select-none items-center", className),
496
- ...props,
497
- children: [
498
- /* @__PURE__ */ jsx11(SliderPrimitive.Track, { className: "relative h-2 w-full grow overflow-hidden rounded-full bg-secondary", children: /* @__PURE__ */ jsx11(SliderPrimitive.Range, { className: "absolute h-full bg-primary" }) }),
499
- (props.value ?? props.defaultValue ?? [0]).map((_, i) => /* @__PURE__ */ jsx11(
500
- SliderPrimitive.Thumb,
501
- {
502
- className: cn(
503
- "block h-5 w-5 rounded-full border-2 border-primary bg-background",
504
- "transition-all duration-[var(--duration-normal,200ms)] ease-out shadow-md",
505
- "hover:shadow-lg hover:scale-110",
506
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
507
- "disabled:pointer-events-none disabled:opacity-50"
508
- )
509
- },
510
- i
511
- ))
512
- ]
495
+ var Slider = React10.forwardRef(({ className, thumbLabels, ...props }, ref) => {
496
+ const values = props.value ?? props.defaultValue ?? [0];
497
+ const rootLabel = props["aria-label"];
498
+ const rootLabelledBy = props["aria-labelledby"];
499
+ if (typeof process !== "undefined" && process.env?.NODE_ENV !== "production" && thumbLabels && thumbLabels.length !== values.length) {
500
+ console.warn(
501
+ `Slider: thumbLabels.length (${thumbLabels.length}) does not match value.length (${values.length}). Missing labels fall back to indexed names; extra labels are ignored.`
502
+ );
513
503
  }
514
- ));
504
+ return /* @__PURE__ */ jsxs4(
505
+ SliderPrimitive.Root,
506
+ {
507
+ ref,
508
+ className: cn("relative flex w-full touch-none select-none items-center", className),
509
+ ...props,
510
+ children: [
511
+ /* @__PURE__ */ jsx11(SliderPrimitive.Track, { className: "relative h-2 w-full grow overflow-hidden rounded-full bg-secondary", children: /* @__PURE__ */ jsx11(SliderPrimitive.Range, { className: "absolute h-full bg-primary" }) }),
512
+ values.map((_, i) => {
513
+ const explicit = thumbLabels?.[i];
514
+ const fallback = values.length === 1 ? rootLabel : rootLabel ? `${rootLabel} (${i + 1} of ${values.length})` : void 0;
515
+ return /* @__PURE__ */ jsx11(
516
+ SliderPrimitive.Thumb,
517
+ {
518
+ "aria-label": explicit ?? fallback,
519
+ "aria-labelledby": explicit || fallback ? void 0 : rootLabelledBy,
520
+ className: cn(
521
+ "block h-5 w-5 rounded-full border-2 border-primary bg-background",
522
+ "transition-all duration-[var(--duration-normal,200ms)] ease-out shadow-md",
523
+ "hover:shadow-lg hover:scale-110",
524
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
525
+ "disabled:pointer-events-none disabled:opacity-50"
526
+ )
527
+ },
528
+ i
529
+ );
530
+ })
531
+ ]
532
+ }
533
+ );
534
+ });
515
535
  Slider.displayName = "Slider";
516
536
 
517
537
  // src/primitives/toggle/toggle.tsx
@@ -666,14 +686,24 @@ Progress.displayName = "Progress";
666
686
  import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
667
687
  import * as React15 from "react";
668
688
  import { jsx as jsx17, jsxs as jsxs5 } from "react/jsx-runtime";
669
- var ScrollArea = React15.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs5(
689
+ var ScrollArea = React15.forwardRef(({ className, children, viewportTabIndex = 0, ...props }, ref) => /* @__PURE__ */ jsxs5(
670
690
  ScrollAreaPrimitive.Root,
671
691
  {
672
692
  ref,
673
693
  className: cn("relative overflow-hidden", className),
674
694
  ...props,
675
695
  children: [
676
- /* @__PURE__ */ jsx17(ScrollAreaPrimitive.Viewport, { className: "h-full w-full rounded-[inherit]", children }),
696
+ /* @__PURE__ */ jsx17(
697
+ ScrollAreaPrimitive.Viewport,
698
+ {
699
+ tabIndex: viewportTabIndex,
700
+ className: cn(
701
+ "h-full w-full rounded-[inherit]",
702
+ viewportTabIndex >= 0 && "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
703
+ ),
704
+ children
705
+ }
706
+ ),
677
707
  /* @__PURE__ */ jsx17(ScrollBar, { orientation: "vertical" }),
678
708
  /* @__PURE__ */ jsx17(ScrollBar, { orientation: "horizontal" }),
679
709
  /* @__PURE__ */ jsx17(ScrollAreaPrimitive.Corner, {})
@@ -884,15 +914,15 @@ var DialogOverlay = React19.forwardRef(({ className, ...props }, ref) => /* @__P
884
914
  }
885
915
  ));
886
916
  DialogOverlay.displayName = "DialogOverlay";
887
- var DialogContent = React19.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs7(DialogPortal, { children: [
917
+ var DialogContent = React19.forwardRef(({ className, children, scrollable = true, ...props }, ref) => /* @__PURE__ */ jsxs7(DialogPortal, { children: [
888
918
  /* @__PURE__ */ jsx21(DialogOverlay, {}),
889
919
  /* @__PURE__ */ jsxs7(
890
920
  DialogPrimitive.Content,
891
921
  {
892
922
  ref,
893
923
  className: cn(
894
- "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-[var(--gap-md,1rem)]",
895
- "border bg-background p-[var(--space-6,1.5rem)] shadow-lg rounded-lg",
924
+ "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%]",
925
+ scrollable ? "max-h-[calc(100vh-2rem)] border bg-background shadow-lg rounded-lg" : "gap-[var(--gap-md,1rem)] border bg-background p-[var(--space-6,1.5rem)] shadow-lg rounded-lg",
896
926
  "duration-[var(--duration-normal,200ms)] data-[state=open]:animate-in data-[state=closed]:animate-out",
897
927
  "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
898
928
  "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
@@ -900,12 +930,12 @@ var DialogContent = React19.forwardRef(({ className, children, ...props }, ref)
900
930
  ),
901
931
  ...props,
902
932
  children: [
903
- children,
933
+ scrollable ? /* @__PURE__ */ jsx21("div", { className: "grid gap-[var(--gap-md,1rem)] overflow-y-auto p-[var(--space-6,1.5rem)]", children }) : children,
904
934
  /* @__PURE__ */ jsxs7(
905
935
  DialogPrimitive.Close,
906
936
  {
907
937
  className: cn(
908
- "absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background",
938
+ "absolute right-4 top-4 z-10 rounded-sm opacity-70 ring-offset-background bg-background/80 backdrop-blur-sm",
909
939
  "transition-all duration-[var(--duration-normal,200ms)] ease-out hover:opacity-100",
910
940
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
911
941
  "disabled:pointer-events-none"
@@ -1962,7 +1992,10 @@ var TableCaption = React31.forwardRef(({ className, ...props }, ref) => /* @__PU
1962
1992
  "caption",
1963
1993
  {
1964
1994
  ref,
1965
- className: cn("mt-[var(--space-4,1rem)] text-sm text-muted-foreground", className),
1995
+ className: cn(
1996
+ "caption-bottom mt-[var(--space-4,1rem)] text-sm text-muted-foreground",
1997
+ className
1998
+ ),
1966
1999
  ...props
1967
2000
  }
1968
2001
  ));
@@ -1975,13 +2008,19 @@ import {
1975
2008
  useReactTable
1976
2009
  } from "@tanstack/react-table";
1977
2010
  import { jsx as jsx35, jsxs as jsxs13 } from "react/jsx-runtime";
1978
- function DataTable({ columns, data }) {
2011
+ function DataTable({
2012
+ columns,
2013
+ data,
2014
+ caption,
2015
+ "aria-label": ariaLabel
2016
+ }) {
1979
2017
  const table = useReactTable({
1980
2018
  data,
1981
2019
  columns,
1982
2020
  getCoreRowModel: getCoreRowModel()
1983
2021
  });
1984
- return /* @__PURE__ */ jsx35("div", { className: "rounded-md border", children: /* @__PURE__ */ jsxs13(Table, { children: [
2022
+ return /* @__PURE__ */ jsx35("div", { className: "rounded-md border", children: /* @__PURE__ */ jsxs13(Table, { "aria-label": ariaLabel, children: [
2023
+ caption ? /* @__PURE__ */ jsx35(TableCaption, { children: caption }) : null,
1985
2024
  /* @__PURE__ */ jsx35(TableHeader, { children: table.getHeaderGroups().map((headerGroup) => /* @__PURE__ */ jsx35(TableRow, { children: headerGroup.headers.map((header) => /* @__PURE__ */ jsx35(TableHead, { children: header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext()) }, header.id)) }, headerGroup.id)) }),
1986
2025
  /* @__PURE__ */ jsx35(TableBody, { children: table.getRowModel().rows?.length ? table.getRowModel().rows.map((row) => /* @__PURE__ */ jsx35(TableRow, { "data-state": row.getIsSelected() && "selected", children: row.getVisibleCells().map((cell) => /* @__PURE__ */ jsx35(TableCell, { children: flexRender(cell.column.columnDef.cell, cell.getContext()) }, cell.id)) }, row.id)) : /* @__PURE__ */ jsx35(TableRow, { children: /* @__PURE__ */ jsx35(TableCell, { colSpan: columns.length, className: "h-24 text-center", children: "No results." }) }) })
1987
2026
  ] }) });
@@ -2350,7 +2389,7 @@ function CommandDialog({
2350
2389
  /* @__PURE__ */ jsx40(DialogTitle, { children: title }),
2351
2390
  /* @__PURE__ */ jsx40(DialogDescription, { children: description })
2352
2391
  ] }),
2353
- /* @__PURE__ */ jsx40(DialogContent, { className: "overflow-hidden p-0", children: /* @__PURE__ */ jsx40(Command, { className: "[&_[cmdk-group-heading]]:px-[var(--space-2,0.5rem)] [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-[var(--space-2,0.5rem)] [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-[var(--space-2,0.5rem)] [&_[cmdk-item]]:py-[var(--space-3,0.75rem)] [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5", children }) })
2392
+ /* @__PURE__ */ jsx40(DialogContent, { className: "overflow-hidden p-0", scrollable: false, children: /* @__PURE__ */ jsx40(Command, { className: "[&_[cmdk-group-heading]]:px-[var(--space-2,0.5rem)] [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-[var(--space-2,0.5rem)] [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-[var(--space-2,0.5rem)] [&_[cmdk-item]]:py-[var(--space-3,0.75rem)] [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5", children }) })
2354
2393
  ] });
2355
2394
  }
2356
2395
  var CommandInput = React35.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsxs17("div", { className: "flex items-center border-b px-[var(--space-3,0.75rem)]", "cmdk-input-wrapper": "", children: [
@@ -2409,9 +2448,11 @@ var CommandGroup = React35.forwardRef(({ className, ...props }, ref) => /* @__PU
2409
2448
  ));
2410
2449
  CommandGroup.displayName = "CommandGroup";
2411
2450
  var CommandSeparator = React35.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx40(
2412
- CommandPrimitive.Separator,
2451
+ "div",
2413
2452
  {
2414
2453
  ref,
2454
+ role: "none",
2455
+ "data-cmdk-separator": "",
2415
2456
  className: cn("-mx-[var(--space-1,0.25rem)] h-px bg-border", className),
2416
2457
  ...props
2417
2458
  }
@@ -2458,9 +2499,11 @@ function Combobox({
2458
2499
  emptyText = "No results found.",
2459
2500
  disabled,
2460
2501
  className,
2461
- "aria-label": ariaLabel
2502
+ "aria-label": ariaLabel,
2503
+ "aria-labelledby": ariaLabelledBy
2462
2504
  }) {
2463
2505
  const [open, setOpen] = React36.useState(false);
2506
+ const listboxId = React36.useId();
2464
2507
  const selected = options.find((o) => o.value === value);
2465
2508
  return /* @__PURE__ */ jsxs18(Popover, { open, onOpenChange: setOpen, children: [
2466
2509
  /* @__PURE__ */ jsx41(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs18(
@@ -2470,7 +2513,9 @@ function Combobox({
2470
2513
  role: "combobox",
2471
2514
  "aria-expanded": open,
2472
2515
  "aria-haspopup": "listbox",
2516
+ "aria-controls": open ? listboxId : void 0,
2473
2517
  "aria-label": ariaLabel,
2518
+ "aria-labelledby": ariaLabelledBy,
2474
2519
  disabled,
2475
2520
  className: cn(
2476
2521
  "inline-flex h-[var(--control-height-md,2.5rem)] w-[240px] items-center justify-between gap-[var(--gap-sm,0.5rem)] rounded-md border border-input bg-background px-[var(--space-3,0.75rem)] py-[var(--space-2,0.5rem)] text-sm font-normal transition-all duration-[var(--duration-normal,200ms)] ease-out",
@@ -2502,7 +2547,7 @@ function Combobox({
2502
2547
  ) }),
2503
2548
  /* @__PURE__ */ jsx41(PopoverContent, { className: "w-[240px] p-0", align: "start", children: /* @__PURE__ */ jsxs18(Command, { children: [
2504
2549
  /* @__PURE__ */ jsx41(CommandInput, { placeholder: searchPlaceholder }),
2505
- /* @__PURE__ */ jsxs18(CommandList, { children: [
2550
+ /* @__PURE__ */ jsxs18(CommandList, { id: listboxId, children: [
2506
2551
  /* @__PURE__ */ jsx41(CommandEmpty, { children: emptyText }),
2507
2552
  /* @__PURE__ */ jsx41(CommandGroup, { children: options.map((option) => /* @__PURE__ */ jsxs18(
2508
2553
  CommandItem,
@@ -3706,7 +3751,7 @@ var dialogSchema = {
3706
3751
  "Putting too many primary actions in DialogFooter"
3707
3752
  ],
3708
3753
  relatedComponents: ["alert-dialog", "popover", "sheet"],
3709
- accessibilityNotes: "Radix traps focus, handles Escape to close, and wires aria-labelledby/describedby to DialogTitle/DialogDescription. Always include a DialogTitle.",
3754
+ accessibilityNotes: "Radix traps focus, handles Escape to close, and wires aria-labelledby/describedby to DialogTitle/DialogDescription. Always include a DialogTitle. DialogContent is constrained to `max-h-[calc(100vh-2rem)]` and scrolls internally so long content stays inside the focus trap.",
3710
3755
  tokenBudget: 600
3711
3756
  },
3712
3757
  tags: ["dialog", "modal", "overlay", "popup", "form"]
@@ -4224,6 +4269,24 @@ var sliderSchema = {
4224
4269
  default: "horizontal",
4225
4270
  description: "Slider direction",
4226
4271
  enumValues: ["horizontal", "vertical"]
4272
+ },
4273
+ {
4274
+ name: "aria-label",
4275
+ type: "string",
4276
+ required: false,
4277
+ description: "Accessible label for the slider as a whole. Mirrored onto a single thumb automatically; for range sliders prefer thumbLabels."
4278
+ },
4279
+ {
4280
+ name: "aria-labelledby",
4281
+ type: "string",
4282
+ required: false,
4283
+ description: "Id of an external visible label that names the slider."
4284
+ },
4285
+ {
4286
+ name: "thumbLabels",
4287
+ type: "object",
4288
+ required: false,
4289
+ description: "Per-thumb accessible labels (string[]). Required for range sliders so each thumb has a meaningful name (e.g. ['Minimum price', 'Maximum price']). For a single-thumb slider, the Root's aria-label is mirrored onto the thumb automatically and thumbLabels is only needed when overriding that default."
4227
4290
  }
4228
4291
  ],
4229
4292
  variants: [],
@@ -4253,10 +4316,12 @@ var sliderSchema = {
4253
4316
  "Using Slider for exact values without showing the number",
4254
4317
  "Missing min/max bounds",
4255
4318
  "Using step=1 for fractional values (set step=0.01)",
4256
- "Not providing aria-label when there's no visible label"
4319
+ "Not providing aria-label / aria-labelledby when there's no visible label",
4320
+ "Range slider with only Root aria-label and no thumbLabels \u2014 both thumbs fall back to '(N of 2)' indexed names instead of meaningful per-thumb labels",
4321
+ "thumbLabels.length !== value.length \u2014 extra labels are ignored, missing labels fall back to indexed names (dev-mode warning)"
4257
4322
  ],
4258
4323
  relatedComponents: ["input"],
4259
- accessibilityNotes: "Arrow keys step by step, Home/End jump to min/max, PageUp/PageDown step larger. Radix handles aria-valuemin/max/now. Add aria-label if no visible label.",
4324
+ accessibilityNotes: "Arrow keys step by step, Home/End jump to min/max, PageUp/PageDown step larger. Radix handles aria-valuemin/max/now. Each thumb has its own accessible name: explicit via thumbLabels[i], else mirrored from the Root's aria-label (single thumb) or indexed '(i of N)' fallback (range). Add aria-label / aria-labelledby on the Root when there's no visible label.",
4260
4325
  tokenBudget: 450
4261
4326
  },
4262
4327
  tags: ["slider", "range", "form", "numeric", "input"]
@@ -4683,7 +4748,14 @@ var scrollAreaSchema = {
4683
4748
  description: "When scrollbars are visible",
4684
4749
  enumValues: ["auto", "always", "scroll", "hover"]
4685
4750
  },
4686
- { name: "className", type: "string", required: false, description: "Set dimensions via Tailwind (e.g. h-72 w-48)" }
4751
+ { name: "className", type: "string", required: false, description: "Set dimensions via Tailwind (e.g. h-72 w-48)" },
4752
+ {
4753
+ name: "viewportTabIndex",
4754
+ type: "number",
4755
+ required: false,
4756
+ default: 0,
4757
+ description: "tabIndex applied to the scroll viewport. Defaults to 0 so keyboard users can scroll without a pointer; pass -1 to skip the viewport in the tab order when wrapping decorative or already-keyboard-reachable content."
4758
+ }
4687
4759
  ],
4688
4760
  variants: [],
4689
4761
  slots: [
@@ -4713,10 +4785,11 @@ var scrollAreaSchema = {
4713
4785
  commonMistakes: [
4714
4786
  "Forgetting to set height/width \u2014 scrollbars don't appear",
4715
4787
  "Using for the whole page",
4716
- "Nesting ScrollAreas (confusing UX)"
4788
+ "Nesting ScrollAreas (confusing UX)",
4789
+ "Wrapping decorative or already-keyboard-reachable content without setting viewportTabIndex={-1} \u2014 adds an unnecessary tab stop"
4717
4790
  ],
4718
4791
  relatedComponents: [],
4719
- accessibilityNotes: "Radix doesn't make the viewport focusable by default \u2014 add tabIndex={0} on a focusable child or the viewport to enable keyboard scrolling (arrow keys, PgUp/PgDn, Home/End). For long lists, consider pagination or virtualization.",
4792
+ accessibilityNotes: "The viewport is keyboard-focusable by default (viewportTabIndex=0) so users can scroll long content via arrow keys / PgUp / PgDn / Home / End without a pointer. Pass viewportTabIndex={-1} when the contents are already in the tab order or purely decorative. For very long lists, consider pagination or virtualization.",
4720
4793
  tokenBudget: 350
4721
4794
  },
4722
4795
  tags: ["scroll-area", "scroll", "overflow", "scrollbar", "layout"]
@@ -5250,7 +5323,19 @@ var dataTableSchema = {
5250
5323
  subcategory: "data",
5251
5324
  props: [
5252
5325
  { name: "columns", type: "object", required: true, description: "ColumnDef<TData, TValue>[] from @tanstack/react-table" },
5253
- { name: "data", type: "object", required: true, description: "Array of row data" }
5326
+ { name: "data", type: "object", required: true, description: "Array of row data" },
5327
+ {
5328
+ name: "caption",
5329
+ type: "ReactNode",
5330
+ required: false,
5331
+ description: "Visible caption rendered below the table; announced by screen readers when entering the table"
5332
+ },
5333
+ {
5334
+ name: "aria-label",
5335
+ type: "string",
5336
+ required: false,
5337
+ description: "Accessible label forwarded as aria-label on the underlying <table>; use when no visible caption is shown"
5338
+ }
5254
5339
  ],
5255
5340
  variants: [],
5256
5341
  slots: [],
@@ -5274,10 +5359,11 @@ var dataTableSchema = {
5274
5359
  "Forgetting getCoreRowModel() (table renders nothing)",
5275
5360
  "Recreating columns array on every render (breaks memoization \u2014 wrap in useMemo or define outside the component)",
5276
5361
  "Using accessorKey with nested paths without accessorFn",
5277
- "Not adding filter/sort row models when those features are needed"
5362
+ "Not adding filter/sort row models when those features are needed",
5363
+ "Shipping a table without `caption` or `aria-label` \u2014 the table is unlabelled to assistive tech"
5278
5364
  ],
5279
5365
  relatedComponents: ["table", "pagination"],
5280
- accessibilityNotes: "Uses semantic <table> via Hex UI Table primitives. Add aria-sort to sortable column headers. Announce filter/sort changes via aria-live for dynamic updates.",
5366
+ accessibilityNotes: "Pass either `caption` (visible) or `aria-label` so screen readers announce the table when the user enters it. Add aria-sort to sortable column headers. Announce filter/sort changes via aria-live for dynamic updates.",
5281
5367
  tokenBudget: 900
5282
5368
  },
5283
5369
  tags: ["data-table", "tanstack", "sortable", "filterable", "paginated"]
@@ -5691,10 +5777,11 @@ var commandSchema = {
5691
5777
  "Forgetting CommandList \u2014 items won't be scrollable or grouped properly",
5692
5778
  "Giving CommandItem non-unique values (breaks filtering and controlled state)",
5693
5779
  "Overriding CommandInput className to remove the border/padding \u2014 breaks the \u2318K icon layout",
5694
- "Not rendering CommandEmpty \u2014 the list looks broken when a search has no matches"
5780
+ "Not rendering CommandEmpty \u2014 the list looks broken when a search has no matches",
5781
+ "Querying CommandSeparator via cmdk's internal Separator state \u2014 Hex UI renders it as a presentational div with role='none' (and the `data-cmdk-separator` attribute preserved for selector compatibility) so it can sit inside CommandList's role=listbox without violating ARIA"
5695
5782
  ],
5696
5783
  relatedComponents: ["combobox", "dialog", "dropdown-menu"],
5697
- accessibilityNotes: "cmdk wires role=listbox/option and aria-activedescendant. Use the `label` prop on Command for a screen-reader-only name when no visible heading exists.",
5784
+ accessibilityNotes: "cmdk wires role=listbox/option and aria-activedescendant. Use the `label` prop on Command for a screen-reader-only name when no visible heading exists. CommandSeparator renders with role='none' (still selectable via `[data-cmdk-separator]`) so listbox-children rules are satisfied.",
5698
5785
  tokenBudget: 900
5699
5786
  },
5700
5787
  tags: ["command", "cmdk", "palette", "search", "launcher"]
@@ -5758,7 +5845,13 @@ var comboboxSchema = {
5758
5845
  name: "aria-label",
5759
5846
  type: "string",
5760
5847
  required: false,
5761
- description: "Accessible label \u2014 required when no adjacent visible <label> is used"
5848
+ description: "Accessible label \u2014 required when no adjacent visible label is used"
5849
+ },
5850
+ {
5851
+ name: "aria-labelledby",
5852
+ type: "string",
5853
+ required: false,
5854
+ description: "Id of an external visible label that names the combobox"
5762
5855
  }
5763
5856
  ],
5764
5857
  variants: [],
@@ -5789,10 +5882,10 @@ var comboboxSchema = {
5789
5882
  "Using the label as the value \u2014 fine if stable, but prefer a short stable `value` string",
5790
5883
  "Forgetting to bind value + onChange \u2014 uncontrolled mode doesn't exist on this wrapper",
5791
5884
  "Mixing translated labels without keying on value \u2014 label changes won't update selection",
5792
- "Missing aria-label when there's no adjacent visible <label>"
5885
+ "Missing aria-label / aria-labelledby \u2014 role='combobox' does not allow name from contents, so without one of these the trigger has no accessible name"
5793
5886
  ],
5794
5887
  relatedComponents: ["command", "popover", "select"],
5795
- accessibilityNotes: "Trigger has role='combobox' + aria-expanded. Popover traps focus inside the CommandInput. Options are announced via cmdk's aria-activedescendant wiring.",
5888
+ accessibilityNotes: "Trigger has role='combobox' + aria-expanded + aria-haspopup='listbox'. aria-controls points at the inner CommandList (a useId-stabilized listbox). Pass aria-label or aria-labelledby \u2014 combobox does not derive its name from contents.",
5796
5889
  tokenBudget: 900
5797
5890
  },
5798
5891
  tags: ["combobox", "select", "search", "cmdk", "input"]