@seekalfred/primitives 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/dist/accordion/accordion.d.ts +5 -5
  2. package/dist/accordion/accordion.js +13 -8
  3. package/dist/accordion/accordion.js.map +1 -1
  4. package/dist/alert/alert.d.ts +1 -1
  5. package/dist/alert/alert.js +4 -2
  6. package/dist/alert/alert.js.map +1 -1
  7. package/dist/alert-dialog/alert-dialog.d.ts +1 -1
  8. package/dist/alert-dialog/alert-dialog.js +4 -2
  9. package/dist/alert-dialog/alert-dialog.js.map +1 -1
  10. package/dist/avatar/avatar.d.ts +1 -1
  11. package/dist/avatar/avatar.js +6 -4
  12. package/dist/avatar/avatar.js.map +1 -1
  13. package/dist/badge/badge.d.ts +3 -3
  14. package/dist/badge/badge.js +4 -2
  15. package/dist/badge/badge.js.map +1 -1
  16. package/dist/breadcrumb/breadcrumb.d.ts +3 -3
  17. package/dist/breadcrumb/breadcrumb.js +7 -4
  18. package/dist/breadcrumb/breadcrumb.js.map +1 -1
  19. package/dist/button/button.d.ts +3 -3
  20. package/dist/button/button.js +4 -2
  21. package/dist/button/button.js.map +1 -1
  22. package/dist/calendar/calendar.d.ts +2 -2
  23. package/dist/calendar/calendar.js +7 -5
  24. package/dist/calendar/calendar.js.map +1 -1
  25. package/dist/card/card.d.ts +6 -6
  26. package/dist/card/card.js +19 -12
  27. package/dist/card/card.js.map +1 -1
  28. package/dist/checkbox/checkbox.d.ts +2 -2
  29. package/dist/checkbox/checkbox.js +4 -2
  30. package/dist/checkbox/checkbox.js.map +1 -1
  31. package/dist/dialog/dialog.d.ts +3 -3
  32. package/dist/dialog/dialog.js +7 -4
  33. package/dist/dialog/dialog.js.map +1 -1
  34. package/dist/input/input.d.ts +1 -1
  35. package/dist/input/input.js +1 -1
  36. package/dist/label/label.d.ts +1 -1
  37. package/dist/label/label.js +4 -2
  38. package/dist/label/label.js.map +1 -1
  39. package/dist/lib/merge-refs.js +22 -0
  40. package/dist/lib/merge-refs.js.map +1 -0
  41. package/dist/menu/menu.d.ts +5 -5
  42. package/dist/menu/menu.js +10 -6
  43. package/dist/menu/menu.js.map +1 -1
  44. package/dist/popover/popover.d.ts +2 -2
  45. package/dist/popover/popover.js +4 -2
  46. package/dist/popover/popover.js.map +1 -1
  47. package/dist/progress/progress.d.ts +2 -2
  48. package/dist/progress/progress.js +4 -2
  49. package/dist/progress/progress.js.map +1 -1
  50. package/dist/radio-group/radio-group.d.ts +3 -3
  51. package/dist/radio-group/radio-group.js +7 -4
  52. package/dist/radio-group/radio-group.js.map +1 -1
  53. package/dist/scroll-area/scroll-area.d.ts +7 -6
  54. package/dist/scroll-area/scroll-area.js +6 -4
  55. package/dist/scroll-area/scroll-area.js.map +1 -1
  56. package/dist/select/select.d.ts +3 -3
  57. package/dist/select/select.js +7 -4
  58. package/dist/select/select.js.map +1 -1
  59. package/dist/separator/separator.d.ts +1 -1
  60. package/dist/separator/separator.js +4 -2
  61. package/dist/separator/separator.js.map +1 -1
  62. package/dist/sheet/sheet.d.ts +1 -1
  63. package/dist/sheet/sheet.js +4 -2
  64. package/dist/sheet/sheet.js.map +1 -1
  65. package/dist/skeleton/skeleton.d.ts +1 -1
  66. package/dist/skeleton/skeleton.js +4 -2
  67. package/dist/skeleton/skeleton.js.map +1 -1
  68. package/dist/slider/slider.d.ts +2 -2
  69. package/dist/slider/slider.js +4 -2
  70. package/dist/slider/slider.js.map +1 -1
  71. package/dist/spinner/spinner.d.ts +1 -1
  72. package/dist/spinner/spinner.js +4 -2
  73. package/dist/spinner/spinner.js.map +1 -1
  74. package/dist/switch/switch.d.ts +2 -2
  75. package/dist/switch/switch.js +4 -2
  76. package/dist/switch/switch.js.map +1 -1
  77. package/dist/table/table.d.ts +4 -4
  78. package/dist/table/table.js +4 -3
  79. package/dist/table/table.js.map +1 -1
  80. package/dist/tabs/tabs.d.ts +4 -4
  81. package/dist/tabs/tabs.js +13 -8
  82. package/dist/tabs/tabs.js.map +1 -1
  83. package/dist/textarea/textarea.d.ts +1 -1
  84. package/dist/textarea/textarea.js +4 -2
  85. package/dist/textarea/textarea.js.map +1 -1
  86. package/dist/toast/toast.d.ts +2 -2
  87. package/dist/toast/toast.js +4 -2
  88. package/dist/toast/toast.js.map +1 -1
  89. package/dist/tooltip/tooltip.d.ts +2 -2
  90. package/dist/tooltip/tooltip.js +4 -2
  91. package/dist/tooltip/tooltip.js.map +1 -1
  92. package/package.json +3 -3
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { cn } from "../lib/cn.js";
3
3
  import { cva } from "class-variance-authority";
4
+ import { forwardRef } from "react";
4
5
  import { Button, Dialog, DialogTrigger, Heading, Modal, ModalOverlay, Text } from "react-aria-components";
5
6
  import { jsx, jsxs } from "react/jsx-runtime";
6
7
  //#region src/dialog/dialog.tsx
@@ -68,7 +69,7 @@ const dialogCloseVariants = cva([
68
69
  "data-[focus-visible]:outline-(--color-ring)",
69
70
  "[&_svg]:pointer-events-none [&_svg]:size-4"
70
71
  ]);
71
- function Dialog$1({ title, beforeTitle, description, children, className, closeButton = true, closeLabel = "Close", role = "dialog", isDismissable = true, overlayClassName, ...props }) {
72
+ const Dialog$1 = forwardRef(function Dialog$2({ title, beforeTitle, description, children, className, closeButton = true, closeLabel = "Close", role = "dialog", isDismissable = true, overlayClassName, ...props }, ref) {
72
73
  return /* @__PURE__ */ jsx(ModalOverlay, {
73
74
  ...props,
74
75
  isDismissable,
@@ -77,6 +78,7 @@ function Dialog$1({ title, beforeTitle, description, children, className, closeB
77
78
  children: /* @__PURE__ */ jsx(Modal, {
78
79
  className: "contents",
79
80
  children: /* @__PURE__ */ jsxs(Dialog, {
81
+ ref,
80
82
  role,
81
83
  "data-slot": "dialog",
82
84
  className: cn(dialogPanelVariants(), className),
@@ -113,14 +115,15 @@ function Dialog$1({ title, beforeTitle, description, children, className, closeB
113
115
  })
114
116
  })
115
117
  });
116
- }
117
- function DialogFooter({ children, className }) {
118
+ });
119
+ const DialogFooter = forwardRef(function DialogFooter({ children, className }, ref) {
118
120
  return /* @__PURE__ */ jsx("div", {
121
+ ref,
119
122
  "data-slot": "dialog-footer",
120
123
  className: cn(dialogFooterVariants(), className),
121
124
  children
122
125
  });
123
- }
126
+ });
124
127
  //#endregion
125
128
  export { Dialog$1 as Dialog, DialogFooter, DialogTrigger, dialogDescriptionVariants, dialogFooterVariants, dialogOverlayVariants, dialogPanelVariants, dialogTitleVariants };
126
129
 
@@ -1 +1 @@
1
- {"version":3,"file":"dialog.js","names":["Dialog","AriaModalOverlay","AriaModal","AriaDialog","AriaHeading","AriaText","AriaButton"],"sources":["../../src/dialog/dialog.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport type { ReactNode } from \"react\";\nimport {\n Button as AriaButton,\n Dialog as AriaDialog,\n Heading as AriaHeading,\n Modal as AriaModal,\n ModalOverlay as AriaModalOverlay,\n Text as AriaText,\n type ModalOverlayProps as AriaModalOverlayProps,\n} from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\n\n/**\n * Dialog, ported from the legacy shadcn/Radix component onto React Aria's\n * Modal primitives.\n *\n * FOUR LEGACY PARTS COLLAPSE INTO TWO PROPS. The census (9 importing files,\n * but only **7** that render anything -- header.tsx and PollQuizCreator.tsx\n * import the parts and use none) reads: DialogContent 9, DialogTitle 8,\n * DialogHeader 7, DialogDescription 2. Those four always appear in the same\n * shape, so `title` and `description` are props of the dialog itself.\n *\n * That is not tidying. `title` is REQUIRED, which makes an unnamed dialog\n * impossible to write -- and the legacy shipped one: LiveQAOverlay.tsx\n * renders a DialogContent with no DialogTitle, so its `aria-labelledby`\n * points at an id that matches no element and the dialog is announced with\n * no name.\n *\n * It also fixes the dangling-description defect for free. Radix set\n * `aria-describedby` unconditionally from a `useId`, so seven of the nine\n * legacy dialogs referenced an id that did not exist. React Aria wires it\n * only when a description slot is actually rendered.\n *\n * `DialogPortal` and `DialogOverlay` are not ported: zero external call\n * sites, and React Aria's ModalOverlay does both jobs.\n *\n * NOT PORTED, and deliberately: the enter/exit animation. The legacy leans\n * on tailwindcss-animate's `animate-in` / `zoom-in-95` utilities, which this\n * repo does not ship, and the motion tokens are a separate task. React Aria\n * exposes `data-entering` / `data-exiting` for whoever adds them.\n *\n * Every string in the cva blocks below is a STATIC literal. Tailwind scans\n * source text, so a class assembled at runtime is invisible to it and would\n * simply never be emitted.\n */\nexport const dialogOverlayVariants = cva([\n // Centring by flex, not by `top-1/2 left-1/2 -translate-1/2`. The legacy's\n // translate pair is a bracket value that check:hardcoding rejects, and\n // flex centring needs no transform to undo.\n \"fixed inset-0 z-50 flex items-center justify-center\",\n // The inset that keeps a full-width panel off the viewport edge. The\n // legacy did this with max-w-[calc(100%-2rem)] -- also a bracket value.\n \"p-4\",\n \"bg-scrim\",\n]);\n\nexport const dialogPanelVariants = cva([\n // max-w-128 (32rem), NOT max-w-lg. `--container-lg` does not exist in this\n // repo -- the container scale holds only page-shell widths (max 75rem,\n // wide 90rem) -- so `max-w-lg` compiles to NOTHING and the panel spans the\n // viewport. It emitted no rule at all, tsc and ESLint were both blind to\n // it, and a screenshot is what caught it. 128 spacing units is 32rem,\n // exactly the legacy sm:max-w-lg.\n \"relative w-full max-w-128\",\n \"rounded-xl border-(length:--border-width) border-border p-6\",\n // bg-card, not bg-background: a dialog is a raised surface, and card is\n // the role that already steps up the ink ramp on dark, where a shadow made\n // of ink is invisible. Its contrast pairs are in the contract already.\n \"bg-card text-card-foreground shadow-modal\",\n \"flex flex-col gap-4\",\n // The reset in @seekalfred/styles is OPT-IN, so a consumer may load none:\n // content-box would add the 24px padding to max-w-lg, and an unstyled\n // document renders text in a serif. The Accordion shipped its panel that\n // way and the Alert overflowed by 46px.\n \"box-border font-sans\",\n // React Aria gives the dialog tabindex=-1 and focuses it when nothing\n // inside is autofocused, so it can receive a visible focus ring it does\n // not want.\n \"outline-none\",\n // A dialog taller than the viewport has to scroll, or its footer is\n // unreachable. max-h is on the panel rather than the overlay so the\n // padding above stays visible.\n \"max-h-full overflow-y-auto\",\n]);\n\nexport const dialogTitleVariants = cva([\n // leading-* AFTER text-*: tailwind-merge treats a later text-* as owning\n // line-height and silently strips a leading-* that precedes it.\n \"text-lg leading-(--leading-tight) font-semibold\",\n // The user-agent <h2> ships a 1em block margin. The legacy was inside a\n // Preflight'd app and never met it.\n \"m-0\",\n]);\n\nexport const dialogDescriptionVariants = cva([\n \"text-sm leading-(--leading-normal) text-muted-foreground\",\n // Same: the user-agent <p> block margin.\n \"m-0\",\n]);\n\nexport const dialogFooterVariants = cva([\n // NOT flex-col-reverse. The legacy reversed the footer below `sm`, so a\n // DOM order of Cancel-then-Confirm rendered Confirm-then-Cancel on a\n // phone -- WCAG 1.3.2 Meaningful Sequence, and both alert-dialog call\n // sites hit it. Wrapping keeps narrow viewports working without lying\n // about the order.\n \"flex flex-wrap items-center justify-end gap-2\",\n]);\n\nconst dialogCloseVariants = cva([\n \"absolute end-4 top-4 inline-flex size-6 items-center justify-center\",\n \"rounded-2xs box-border cursor-pointer\",\n \"border-(length:--border-width) border-transparent bg-transparent\",\n \"text-muted-foreground\",\n \"motion-safe:transition-colors motion-safe:duration-(--duration-fast) motion-safe:ease-standard\",\n // The legacy sat at opacity-70 until hover, dimming the only close\n // affordance below its already-marginal contrast. A token that is legible\n // at rest does the same job without that.\n \"data-[hovered]:text-foreground\",\n // outline-solid is load-bearing: outline-none sets --tw-outline-style:none,\n // and the width utility resolves its style from that variable, so a focus\n // rule can apply a width and a colour and paint nothing. Button shipped\n // exactly that.\n \"outline-none\",\n \"data-[focus-visible]:outline-solid\",\n \"data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"data-[focus-visible]:outline-offset-(--focus-ring-offset)\",\n \"data-[focus-visible]:outline-(--color-ring)\",\n \"[&_svg]:pointer-events-none [&_svg]:size-4\",\n]);\n\n/**\n * A modal dialog.\n *\n * @example\n * <Dialog isOpen={open} onOpenChange={setOpen} title=\"Delete user\"\n * description=\"This cannot be undone.\">\n * <DialogFooter>\n * <Button slot=\"close\" variant=\"ghost\">Cancel</Button>\n * <Button variant=\"destructive\" onPress={remove}>Delete</Button>\n * </DialogFooter>\n * </Dialog>\n */\nexport interface DialogProps\n extends Omit<AriaModalOverlayProps, \"className\" | \"style\" | \"children\"> {\n /**\n * The dialog's heading, and its accessible name.\n *\n * **Required.** The legacy allowed a dialog with no title and one call\n * site shipped that, producing an `aria-labelledby` pointing at nothing.\n * Making it a required prop turns that into something you cannot express.\n */\n title: ReactNode;\n /**\n * Rendered ABOVE the heading, inside the panel.\n *\n * Exists for Sheet's back control. A panel that replaces its own contents\n * rather than stacking a second modal needs a way back, and that control\n * cannot live in `title`: React Aria wires aria-labelledby to the heading,\n * so a button inside it becomes part of the dialog's accessible name --\n * \"Back Campaign details\" instead of \"Campaign details\".\n */\n beforeTitle?: ReactNode;\n /** Optional supporting text, wired to `aria-describedby` when present. */\n description?: ReactNode;\n /** The dialog body. */\n children?: ReactNode;\n /** Extra classes for the panel. Merged last, so they win conflicts. */\n className?: string;\n /**\n * Extra classes for the scrim, merged last.\n *\n * A centred modal never needs this. A Sheet does: it has to replace the\n * overlay's centring with an edge alignment, and it is the only way to\n * do that without a second copy of the whole overlay.\n */\n overlayClassName?: string;\n /**\n * Whether to render the corner close button.\n *\n * `true` by default. An `alertdialog` sets it `false`: a confirmation must\n * be answered, not dismissed silently.\n */\n closeButton?: boolean;\n /**\n * Accessible name for the close button.\n *\n * A prop because the legacy hard-coded the English string `Close` in an\n * `sr-only` span with no way to override it, which makes the component\n * unlocalisable.\n *\n * @default \"Close\"\n */\n closeLabel?: string;\n /** `alertdialog` for a destructive confirmation. @default \"dialog\" */\n role?: \"dialog\" | \"alertdialog\";\n /**\n * Whether clicking outside closes the dialog.\n *\n * `true`, which is NOT React Aria's default. React Aria ships\n * `isDismissable = false` (useOverlay.mjs:26); Radix, which the legacy\n * component wraps, closes on outside click unless told otherwise. Taking\n * the headless default here would have silently changed the behaviour of\n * all seven legacy call sites, and nothing in the suite would have\n * noticed.\n *\n * An `alertdialog` should set this `false`: a confirmation must be\n * answered, not clicked away.\n *\n * @default true\n */\n isDismissable?: boolean;\n}\n\nexport function Dialog({\n title,\n beforeTitle,\n description,\n children,\n className,\n closeButton = true,\n closeLabel = \"Close\",\n role = \"dialog\",\n isDismissable = true,\n overlayClassName,\n ...props\n}: DialogProps) {\n return (\n // ModalOverlay explicitly, not a bare <Modal>. A bare Modal still renders\n // an overlay div, but className lands on the INNER element -- so the\n // scrim would be unstyleable and would paint nothing at all.\n <AriaModalOverlay\n {...props}\n isDismissable={isDismissable}\n data-slot=\"dialog-overlay\"\n className={cn(dialogOverlayVariants(), overlayClassName)}\n >\n <AriaModal className=\"contents\">\n <AriaDialog\n role={role}\n data-slot=\"dialog\"\n className={cn(dialogPanelVariants(), className)}\n >\n {/*\n Above the heading, not inside it. Sheet's back control lives here:\n folding it into `title` would put a button inside the <h2> React\n Aria wires aria-labelledby to, so the dialog's accessible name\n would become \"Back Campaign details\".\n */}\n {beforeTitle}\n {/*\n slot=\"title\" is what React Aria wires aria-labelledby to. A plain\n <h2> here would render identically and leave the dialog unnamed.\n */}\n <AriaHeading slot=\"title\" data-slot=\"dialog-title\" className={dialogTitleVariants()}>\n {title}\n </AriaHeading>\n {description == null ? null : (\n <AriaText\n slot=\"description\"\n data-slot=\"dialog-description\"\n className={dialogDescriptionVariants()}\n >\n {description}\n </AriaText>\n )}\n {children}\n {closeButton ? (\n // slot=\"close\" is React Aria's own dismiss wiring; an onPress\n // that called onOpenChange by hand would not work inside an\n // uncontrolled DialogTrigger.\n <AriaButton slot=\"close\" aria-label={closeLabel} className={dialogCloseVariants()}>\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n >\n <path d=\"m4 4 8 8M12 4l-8 8\" />\n </svg>\n </AriaButton>\n ) : null}\n </AriaDialog>\n </AriaModal>\n </AriaModalOverlay>\n );\n}\n\n/**\n * Actions at the foot of a dialog.\n *\n * @example\n * <DialogFooter>\n * <Button slot=\"close\" variant=\"ghost\">Cancel</Button>\n * <Button variant=\"destructive\" onPress={remove}>Delete</Button>\n * </DialogFooter>\n */\nexport interface DialogFooterProps {\n children?: ReactNode;\n className?: string;\n}\n\nexport function DialogFooter({ children, className }: DialogFooterProps) {\n return (\n <div data-slot=\"dialog-footer\" className={cn(dialogFooterVariants(), className)}>\n {children}\n </div>\n );\n}\n\n/**\n * Pairs a trigger with a dialog and owns the open state.\n *\n * Re-exported from React Aria unchanged. Only 3 of the 9 legacy call sites\n * use a trigger at all -- the rest drive `isOpen` themselves -- so wrapping\n * it would add a layer with nothing to do.\n */\nexport { DialogTrigger } from \"react-aria-components\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiDA,MAAa,wBAAwB,IAAI;CAIvC;CAGA;CACA;AACF,CAAC;AAED,MAAa,sBAAsB,IAAI;CAOrC;CACA;CAIA;CACA;CAKA;CAIA;CAIA;AACF,CAAC;AAED,MAAa,sBAAsB,IAAI,CAGrC,mDAGA,KACF,CAAC;AAED,MAAa,4BAA4B,IAAI,CAC3C,4DAEA,KACF,CAAC;AAED,MAAa,uBAAuB,IAAI,CAMtC,+CACF,CAAC;AAED,MAAM,sBAAsB,IAAI;CAC9B;CACA;CACA;CACA;CACA;CAIA;CAKA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAqFD,SAAgBA,SAAO,EACrB,OACA,aACA,aACA,UACA,WACA,cAAc,MACd,aAAa,SACb,OAAO,UACP,gBAAgB,MAChB,kBACA,GAAG,SACW;CACd,OAIE,oBAACC,cAAD;EACE,GAAI;EACW;EACf,aAAU;EACV,WAAW,GAAG,sBAAsB,GAAG,gBAAgB;EAEvD,UAAA,oBAACC,OAAD;GAAW,WAAU;GACnB,UAAA,qBAACC,QAAD;IACQ;IACN,aAAU;IACV,WAAW,GAAG,oBAAoB,GAAG,SAAS;IAHhD,UAAA;KAWG;KAKD,oBAACC,SAAD;MAAa,MAAK;MAAQ,aAAU;MAAe,WAAW,oBAAoB;MAC/E,UAAA;KACU,CAAA;KACZ,eAAe,OAAO,OACrB,oBAACC,MAAD;MACE,MAAK;MACL,aAAU;MACV,WAAW,0BAA0B;MAEpC,UAAA;KACO,CAAA;KAEX;KACA,cAIC,oBAACC,QAAD;MAAY,MAAK;MAAQ,cAAY;MAAY,WAAW,oBAAoB;MAC9E,UAAA,oBAAC,OAAD;OACE,eAAY;OACZ,SAAQ;OACR,MAAK;OACL,QAAO;OACP,aAAY;OACZ,eAAc;OAEd,UAAA,oBAAC,QAAD,EAAM,GAAE,qBAAsB,CAAA;MAC3B,CAAA;KACK,CAAA,IACV;IACM;;EACH,CAAA;CACK,CAAA;AAEtB;AAgBA,SAAgB,aAAa,EAAE,UAAU,aAAgC;CACvE,OACE,oBAAC,OAAD;EAAK,aAAU;EAAgB,WAAW,GAAG,qBAAqB,GAAG,SAAS;EAC3E;CACE,CAAA;AAET"}
1
+ {"version":3,"file":"dialog.js","names":["Dialog","AriaModalOverlay","AriaModal","AriaDialog","AriaHeading","AriaText","AriaButton"],"sources":["../../src/dialog/dialog.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, type ReactNode } from \"react\";\nimport {\n Button as AriaButton,\n Dialog as AriaDialog,\n Heading as AriaHeading,\n Modal as AriaModal,\n ModalOverlay as AriaModalOverlay,\n Text as AriaText,\n type ModalOverlayProps as AriaModalOverlayProps,\n} from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\n\n/**\n * Dialog, ported from the legacy shadcn/Radix component onto React Aria's\n * Modal primitives.\n *\n * FOUR LEGACY PARTS COLLAPSE INTO TWO PROPS. The census (9 importing files,\n * but only **7** that render anything -- header.tsx and PollQuizCreator.tsx\n * import the parts and use none) reads: DialogContent 9, DialogTitle 8,\n * DialogHeader 7, DialogDescription 2. Those four always appear in the same\n * shape, so `title` and `description` are props of the dialog itself.\n *\n * That is not tidying. `title` is REQUIRED, which makes an unnamed dialog\n * impossible to write -- and the legacy shipped one: LiveQAOverlay.tsx\n * renders a DialogContent with no DialogTitle, so its `aria-labelledby`\n * points at an id that matches no element and the dialog is announced with\n * no name.\n *\n * It also fixes the dangling-description defect for free. Radix set\n * `aria-describedby` unconditionally from a `useId`, so seven of the nine\n * legacy dialogs referenced an id that did not exist. React Aria wires it\n * only when a description slot is actually rendered.\n *\n * `DialogPortal` and `DialogOverlay` are not ported: zero external call\n * sites, and React Aria's ModalOverlay does both jobs.\n *\n * NOT PORTED, and deliberately: the enter/exit animation. The legacy leans\n * on tailwindcss-animate's `animate-in` / `zoom-in-95` utilities, which this\n * repo does not ship, and the motion tokens are a separate task. React Aria\n * exposes `data-entering` / `data-exiting` for whoever adds them.\n *\n * Every string in the cva blocks below is a STATIC literal. Tailwind scans\n * source text, so a class assembled at runtime is invisible to it and would\n * simply never be emitted.\n */\nexport const dialogOverlayVariants = cva([\n // Centring by flex, not by `top-1/2 left-1/2 -translate-1/2`. The legacy's\n // translate pair is a bracket value that check:hardcoding rejects, and\n // flex centring needs no transform to undo.\n \"fixed inset-0 z-50 flex items-center justify-center\",\n // The inset that keeps a full-width panel off the viewport edge. The\n // legacy did this with max-w-[calc(100%-2rem)] -- also a bracket value.\n \"p-4\",\n \"bg-scrim\",\n]);\n\nexport const dialogPanelVariants = cva([\n // max-w-128 (32rem), NOT max-w-lg. `--container-lg` does not exist in this\n // repo -- the container scale holds only page-shell widths (max 75rem,\n // wide 90rem) -- so `max-w-lg` compiles to NOTHING and the panel spans the\n // viewport. It emitted no rule at all, tsc and ESLint were both blind to\n // it, and a screenshot is what caught it. 128 spacing units is 32rem,\n // exactly the legacy sm:max-w-lg.\n \"relative w-full max-w-128\",\n \"rounded-xl border-(length:--border-width) border-border p-6\",\n // bg-card, not bg-background: a dialog is a raised surface, and card is\n // the role that already steps up the ink ramp on dark, where a shadow made\n // of ink is invisible. Its contrast pairs are in the contract already.\n \"bg-card text-card-foreground shadow-modal\",\n \"flex flex-col gap-4\",\n // The reset in @seekalfred/styles is OPT-IN, so a consumer may load none:\n // content-box would add the 24px padding to max-w-lg, and an unstyled\n // document renders text in a serif. The Accordion shipped its panel that\n // way and the Alert overflowed by 46px.\n \"box-border font-sans\",\n // React Aria gives the dialog tabindex=-1 and focuses it when nothing\n // inside is autofocused, so it can receive a visible focus ring it does\n // not want.\n \"outline-none\",\n // A dialog taller than the viewport has to scroll, or its footer is\n // unreachable. max-h is on the panel rather than the overlay so the\n // padding above stays visible.\n \"max-h-full overflow-y-auto\",\n]);\n\nexport const dialogTitleVariants = cva([\n // leading-* AFTER text-*: tailwind-merge treats a later text-* as owning\n // line-height and silently strips a leading-* that precedes it.\n \"text-lg leading-(--leading-tight) font-semibold\",\n // The user-agent <h2> ships a 1em block margin. The legacy was inside a\n // Preflight'd app and never met it.\n \"m-0\",\n]);\n\nexport const dialogDescriptionVariants = cva([\n \"text-sm leading-(--leading-normal) text-muted-foreground\",\n // Same: the user-agent <p> block margin.\n \"m-0\",\n]);\n\nexport const dialogFooterVariants = cva([\n // NOT flex-col-reverse. The legacy reversed the footer below `sm`, so a\n // DOM order of Cancel-then-Confirm rendered Confirm-then-Cancel on a\n // phone -- WCAG 1.3.2 Meaningful Sequence, and both alert-dialog call\n // sites hit it. Wrapping keeps narrow viewports working without lying\n // about the order.\n \"flex flex-wrap items-center justify-end gap-2\",\n]);\n\nconst dialogCloseVariants = cva([\n \"absolute end-4 top-4 inline-flex size-6 items-center justify-center\",\n \"rounded-2xs box-border cursor-pointer\",\n \"border-(length:--border-width) border-transparent bg-transparent\",\n \"text-muted-foreground\",\n \"motion-safe:transition-colors motion-safe:duration-(--duration-fast) motion-safe:ease-standard\",\n // The legacy sat at opacity-70 until hover, dimming the only close\n // affordance below its already-marginal contrast. A token that is legible\n // at rest does the same job without that.\n \"data-[hovered]:text-foreground\",\n // outline-solid is load-bearing: outline-none sets --tw-outline-style:none,\n // and the width utility resolves its style from that variable, so a focus\n // rule can apply a width and a colour and paint nothing. Button shipped\n // exactly that.\n \"outline-none\",\n \"data-[focus-visible]:outline-solid\",\n \"data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"data-[focus-visible]:outline-offset-(--focus-ring-offset)\",\n \"data-[focus-visible]:outline-(--color-ring)\",\n \"[&_svg]:pointer-events-none [&_svg]:size-4\",\n]);\n\n/**\n * A modal dialog.\n *\n * @example\n * <Dialog isOpen={open} onOpenChange={setOpen} title=\"Delete user\"\n * description=\"This cannot be undone.\">\n * <DialogFooter>\n * <Button slot=\"close\" variant=\"ghost\">Cancel</Button>\n * <Button variant=\"destructive\" onPress={remove}>Delete</Button>\n * </DialogFooter>\n * </Dialog>\n */\nexport interface DialogProps\n extends Omit<AriaModalOverlayProps, \"className\" | \"style\" | \"children\"> {\n /**\n * The dialog's heading, and its accessible name.\n *\n * **Required.** The legacy allowed a dialog with no title and one call\n * site shipped that, producing an `aria-labelledby` pointing at nothing.\n * Making it a required prop turns that into something you cannot express.\n */\n title: ReactNode;\n /**\n * Rendered ABOVE the heading, inside the panel.\n *\n * Exists for Sheet's back control. A panel that replaces its own contents\n * rather than stacking a second modal needs a way back, and that control\n * cannot live in `title`: React Aria wires aria-labelledby to the heading,\n * so a button inside it becomes part of the dialog's accessible name --\n * \"Back Campaign details\" instead of \"Campaign details\".\n */\n beforeTitle?: ReactNode;\n /** Optional supporting text, wired to `aria-describedby` when present. */\n description?: ReactNode;\n /** The dialog body. */\n children?: ReactNode;\n /** Extra classes for the panel. Merged last, so they win conflicts. */\n className?: string;\n /**\n * Extra classes for the scrim, merged last.\n *\n * A centred modal never needs this. A Sheet does: it has to replace the\n * overlay's centring with an edge alignment, and it is the only way to\n * do that without a second copy of the whole overlay.\n */\n overlayClassName?: string;\n /**\n * Whether to render the corner close button.\n *\n * `true` by default. An `alertdialog` sets it `false`: a confirmation must\n * be answered, not dismissed silently.\n */\n closeButton?: boolean;\n /**\n * Accessible name for the close button.\n *\n * A prop because the legacy hard-coded the English string `Close` in an\n * `sr-only` span with no way to override it, which makes the component\n * unlocalisable.\n *\n * @default \"Close\"\n */\n closeLabel?: string;\n /** `alertdialog` for a destructive confirmation. @default \"dialog\" */\n role?: \"dialog\" | \"alertdialog\";\n /**\n * Whether clicking outside closes the dialog.\n *\n * `true`, which is NOT React Aria's default. React Aria ships\n * `isDismissable = false` (useOverlay.mjs:26); Radix, which the legacy\n * component wraps, closes on outside click unless told otherwise. Taking\n * the headless default here would have silently changed the behaviour of\n * all seven legacy call sites, and nothing in the suite would have\n * noticed.\n *\n * An `alertdialog` should set this `false`: a confirmation must be\n * answered, not clicked away.\n *\n * @default true\n */\n isDismissable?: boolean;\n}\n\nexport const Dialog = forwardRef<HTMLElement, DialogProps>(function Dialog(\n {\n title,\n beforeTitle,\n description,\n children,\n className,\n closeButton = true,\n closeLabel = \"Close\",\n role = \"dialog\",\n isDismissable = true,\n overlayClassName,\n ...props\n },\n ref,\n) {\n return (\n // ModalOverlay explicitly, not a bare <Modal>. A bare Modal still renders\n // an overlay div, but className lands on the INNER element -- so the\n // scrim would be unstyleable and would paint nothing at all.\n <AriaModalOverlay\n {...props}\n isDismissable={isDismissable}\n data-slot=\"dialog-overlay\"\n className={cn(dialogOverlayVariants(), overlayClassName)}\n >\n <AriaModal className=\"contents\">\n <AriaDialog\n // Modal is a transparent pass-through (className=\"contents\", no\n // box of its own) and ModalOverlay is the scrim -- AriaDialog is\n // the only one of the three that is the visible panel, so the ref\n // goes here and not on either ancestor.\n ref={ref}\n role={role}\n data-slot=\"dialog\"\n className={cn(dialogPanelVariants(), className)}\n >\n {/*\n Above the heading, not inside it. Sheet's back control lives here:\n folding it into `title` would put a button inside the <h2> React\n Aria wires aria-labelledby to, so the dialog's accessible name\n would become \"Back Campaign details\".\n */}\n {beforeTitle}\n {/*\n slot=\"title\" is what React Aria wires aria-labelledby to. A plain\n <h2> here would render identically and leave the dialog unnamed.\n */}\n <AriaHeading slot=\"title\" data-slot=\"dialog-title\" className={dialogTitleVariants()}>\n {title}\n </AriaHeading>\n {description == null ? null : (\n <AriaText\n slot=\"description\"\n data-slot=\"dialog-description\"\n className={dialogDescriptionVariants()}\n >\n {description}\n </AriaText>\n )}\n {children}\n {closeButton ? (\n // slot=\"close\" is React Aria's own dismiss wiring; an onPress\n // that called onOpenChange by hand would not work inside an\n // uncontrolled DialogTrigger.\n <AriaButton slot=\"close\" aria-label={closeLabel} className={dialogCloseVariants()}>\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n >\n <path d=\"m4 4 8 8M12 4l-8 8\" />\n </svg>\n </AriaButton>\n ) : null}\n </AriaDialog>\n </AriaModal>\n </AriaModalOverlay>\n );\n});\n\n/**\n * Actions at the foot of a dialog.\n *\n * @example\n * <DialogFooter>\n * <Button slot=\"close\" variant=\"ghost\">Cancel</Button>\n * <Button variant=\"destructive\" onPress={remove}>Delete</Button>\n * </DialogFooter>\n */\nexport interface DialogFooterProps {\n children?: ReactNode;\n className?: string;\n}\n\nexport const DialogFooter = forwardRef<HTMLDivElement, DialogFooterProps>(function DialogFooter(\n { children, className },\n ref,\n) {\n return (\n <div ref={ref} data-slot=\"dialog-footer\" className={cn(dialogFooterVariants(), className)}>\n {children}\n </div>\n );\n});\n\n/**\n * Pairs a trigger with a dialog and owns the open state.\n *\n * Re-exported from React Aria unchanged. Only 3 of the 9 legacy call sites\n * use a trigger at all -- the rest drive `isOpen` themselves -- so wrapping\n * it would add a layer with nothing to do.\n */\nexport { DialogTrigger } from \"react-aria-components\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiDA,MAAa,wBAAwB,IAAI;CAIvC;CAGA;CACA;AACF,CAAC;AAED,MAAa,sBAAsB,IAAI;CAOrC;CACA;CAIA;CACA;CAKA;CAIA;CAIA;AACF,CAAC;AAED,MAAa,sBAAsB,IAAI,CAGrC,mDAGA,KACF,CAAC;AAED,MAAa,4BAA4B,IAAI,CAC3C,4DAEA,KACF,CAAC;AAED,MAAa,uBAAuB,IAAI,CAMtC,+CACF,CAAC;AAED,MAAM,sBAAsB,IAAI;CAC9B;CACA;CACA;CACA;CACA;CAIA;CAKA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAqFD,MAAaA,WAAS,WAAqC,SAASA,SAClE,EACE,OACA,aACA,aACA,UACA,WACA,cAAc,MACd,aAAa,SACb,OAAO,UACP,gBAAgB,MAChB,kBACA,GAAG,SAEL,KACA;CACA,OAIE,oBAACC,cAAD;EACE,GAAI;EACW;EACf,aAAU;EACV,WAAW,GAAG,sBAAsB,GAAG,gBAAgB;EAEvD,UAAA,oBAACC,OAAD;GAAW,WAAU;GACnB,UAAA,qBAACC,QAAD;IAKO;IACC;IACN,aAAU;IACV,WAAW,GAAG,oBAAoB,GAAG,SAAS;IARhD,UAAA;KAgBG;KAKD,oBAACC,SAAD;MAAa,MAAK;MAAQ,aAAU;MAAe,WAAW,oBAAoB;MAC/E,UAAA;KACU,CAAA;KACZ,eAAe,OAAO,OACrB,oBAACC,MAAD;MACE,MAAK;MACL,aAAU;MACV,WAAW,0BAA0B;MAEpC,UAAA;KACO,CAAA;KAEX;KACA,cAIC,oBAACC,QAAD;MAAY,MAAK;MAAQ,cAAY;MAAY,WAAW,oBAAoB;MAC9E,UAAA,oBAAC,OAAD;OACE,eAAY;OACZ,SAAQ;OACR,MAAK;OACL,QAAO;OACP,aAAY;OACZ,eAAc;OAEd,UAAA,oBAAC,QAAD,EAAM,GAAE,qBAAsB,CAAA;MAC3B,CAAA;KACK,CAAA,IACV;IACM;;EACH,CAAA;CACK,CAAA;AAEtB,CAAC;AAgBD,MAAa,eAAe,WAA8C,SAAS,aACjF,EAAE,UAAU,aACZ,KACA;CACA,OACE,oBAAC,OAAD;EAAU;EAAK,aAAU;EAAgB,WAAW,GAAG,qBAAqB,GAAG,SAAS;EACrF;CACE,CAAA;AAET,CAAC"}
@@ -1,5 +1,5 @@
1
- import { InputProps } from "react-aria-components";
2
1
  import { ReactNode } from "react";
2
+ import { InputProps } from "react-aria-components";
3
3
  //#region src/input/input.d.ts
4
4
  /**
5
5
  * Input, ported from the legacy shadcn component onto React Aria's Input.
@@ -2,9 +2,9 @@
2
2
  import { cn } from "../lib/cn.js";
3
3
  import { fieldSurfaceVariants } from "../lib/field-surface.js";
4
4
  import { cva } from "class-variance-authority";
5
+ import { forwardRef } from "react";
5
6
  import { Input } from "react-aria-components";
6
7
  import { jsx, jsxs } from "react/jsx-runtime";
7
- import { forwardRef } from "react";
8
8
  //#region src/input/input.tsx
9
9
  /**
10
10
  * Input, ported from the legacy shadcn component onto React Aria's Input.
@@ -35,7 +35,7 @@ interface LabelProps$1 extends Omit<LabelProps, "className" | "style"> {
35
35
  /** Extra classes. Merged last, so they win conflicts with ours. */
36
36
  className?: string;
37
37
  }
38
- declare function Label$1({ className, ...props }: LabelProps$1): import("react").JSX.Element;
38
+ declare const Label$1: import("react").ForwardRefExoticComponent<LabelProps$1 & import("react").RefAttributes<HTMLLabelElement>>;
39
39
  //#endregion
40
40
  export { Label$1 as Label, LabelProps$1 as LabelProps };
41
41
  //# sourceMappingURL=label.d.ts.map
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { cn } from "../lib/cn.js";
3
3
  import { cva } from "class-variance-authority";
4
+ import { forwardRef } from "react";
4
5
  import { Label } from "react-aria-components";
5
6
  import { jsx } from "react/jsx-runtime";
6
7
  //#region src/label/label.tsx
@@ -31,13 +32,14 @@ const labelVariants = cva([
31
32
  "select-none",
32
33
  "box-border"
33
34
  ]);
34
- function Label$1({ className, ...props }) {
35
+ const Label$1 = forwardRef(function Label$2({ className, ...props }, ref) {
35
36
  return /* @__PURE__ */ jsx(Label, {
37
+ ref,
36
38
  ...props,
37
39
  "data-slot": "label",
38
40
  className: cn(labelVariants(), className)
39
41
  });
40
- }
42
+ });
41
43
  //#endregion
42
44
  export { Label$1 as Label, labelVariants };
43
45
 
@@ -1 +1 @@
1
- {"version":3,"file":"label.js","names":["Label","AriaLabel"],"sources":["../../src/label/label.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { Label as AriaLabel, type LabelProps as AriaLabelProps } from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\n\n/**\n * Label, ported from the legacy Radix Label onto React Aria's.\n *\n * Census: 11 files, 39 uses. 26 pass `htmlFor`, 19 pass `className`, and\n * nothing else recurs -- so the surface really is \"a styled <label>\".\n *\n * Two legacy rules are NOT ported, and both for the same reason: they\n * depend on markup this component cannot see.\n *\n * - `peer-disabled:cursor-not-allowed peer-disabled:opacity-50` requires the\n * consumer to have put `peer` on a sibling input. Nothing in this library\n * does, and a rule that silently does nothing unless the caller happens to\n * add a class is worse than no rule.\n * - `group-data-[disabled=true]:*` requires an ancestor marked `group` with\n * `data-disabled=\"true\"`. React Aria fields set `data-disabled` on their\n * own wrapper and supply it through LabelContext, so when TextField lands\n * the disabled treatment comes from the field rather than from a class\n * the caller remembered.\n *\n * Every string in the cva block below is a STATIC literal.\n */\nexport const labelVariants = cva([\n \"inline-flex items-center gap-2\",\n // leading-* AFTER text-*: tailwind-merge treats a later text-* as owning\n // line-height and silently strips a leading-* that precedes it.\n \"font-sans text-sm leading-(--leading-none) font-medium text-foreground\",\n \"select-none\",\n // The reset in @seekalfred/styles is OPT-IN, so a consumer may load none.\n // A bare <label> inherits the host page's serif; the Accordion shipped its\n // panel exactly that way.\n \"box-border\",\n]);\n\n/**\n * A form label.\n *\n * Inside a React Aria field (`TextField`, `Checkbox`, ...) the association\n * is automatic through context. Standalone, pass `htmlFor`.\n *\n * @example\n * <Label htmlFor=\"email\">Email</Label>\n */\nexport interface LabelProps extends Omit<AriaLabelProps, \"className\" | \"style\"> {\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nexport function Label({ className, ...props }: LabelProps) {\n return <AriaLabel {...props} data-slot=\"label\" className={cn(labelVariants(), className)} />;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,MAAa,gBAAgB,IAAI;CAC/B;CAGA;CACA;CAIA;AACF,CAAC;AAgBD,SAAgBA,QAAM,EAAE,WAAW,GAAG,SAAqB;CACzD,OAAO,oBAACC,OAAD;EAAW,GAAI;EAAO,aAAU;EAAQ,WAAW,GAAG,cAAc,GAAG,SAAS;CAAI,CAAA;AAC7F"}
1
+ {"version":3,"file":"label.js","names":["Label","AriaLabel"],"sources":["../../src/label/label.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef } from \"react\";\nimport { Label as AriaLabel, type LabelProps as AriaLabelProps } from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\n\n/**\n * Label, ported from the legacy Radix Label onto React Aria's.\n *\n * Census: 11 files, 39 uses. 26 pass `htmlFor`, 19 pass `className`, and\n * nothing else recurs -- so the surface really is \"a styled <label>\".\n *\n * Two legacy rules are NOT ported, and both for the same reason: they\n * depend on markup this component cannot see.\n *\n * - `peer-disabled:cursor-not-allowed peer-disabled:opacity-50` requires the\n * consumer to have put `peer` on a sibling input. Nothing in this library\n * does, and a rule that silently does nothing unless the caller happens to\n * add a class is worse than no rule.\n * - `group-data-[disabled=true]:*` requires an ancestor marked `group` with\n * `data-disabled=\"true\"`. React Aria fields set `data-disabled` on their\n * own wrapper and supply it through LabelContext, so when TextField lands\n * the disabled treatment comes from the field rather than from a class\n * the caller remembered.\n *\n * Every string in the cva block below is a STATIC literal.\n */\nexport const labelVariants = cva([\n \"inline-flex items-center gap-2\",\n // leading-* AFTER text-*: tailwind-merge treats a later text-* as owning\n // line-height and silently strips a leading-* that precedes it.\n \"font-sans text-sm leading-(--leading-none) font-medium text-foreground\",\n \"select-none\",\n // The reset in @seekalfred/styles is OPT-IN, so a consumer may load none.\n // A bare <label> inherits the host page's serif; the Accordion shipped its\n // panel exactly that way.\n \"box-border\",\n]);\n\n/**\n * A form label.\n *\n * Inside a React Aria field (`TextField`, `Checkbox`, ...) the association\n * is automatic through context. Standalone, pass `htmlFor`.\n *\n * @example\n * <Label htmlFor=\"email\">Email</Label>\n */\nexport interface LabelProps extends Omit<AriaLabelProps, \"className\" | \"style\"> {\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nexport const Label = forwardRef<HTMLLabelElement, LabelProps>(function Label(\n { className, ...props },\n ref,\n) {\n return (\n <AriaLabel ref={ref} {...props} data-slot=\"label\" className={cn(labelVariants(), className)} />\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,MAAa,gBAAgB,IAAI;CAC/B;CAGA;CACA;CAIA;AACF,CAAC;AAgBD,MAAaA,UAAQ,WAAyC,SAASA,QACrE,EAAE,WAAW,GAAG,SAChB,KACA;CACA,OACE,oBAACC,OAAD;EAAgB;EAAK,GAAI;EAAO,aAAU;EAAQ,WAAW,GAAG,cAAc,GAAG,SAAS;CAAI,CAAA;AAElG,CAAC"}
@@ -0,0 +1,22 @@
1
+ //#region src/lib/merge-refs.ts
2
+ /**
3
+ * Combines a caller-supplied ref with a ref this component already needs for
4
+ * its own purposes, so both receive the same node.
5
+ *
6
+ * Only two call sites need this -- Avatar's `measure` (image decode-state)
7
+ * and ScrollArea's `region` (scrollTop reset) -- both already attached to the
8
+ * same node the newly forwarded ref now also targets. A shared five-line
9
+ * helper for two call sites is the Rule of Two; anything more general (a
10
+ * `useMergedRef` hook, React 19 ref-cleanup support) is speculative until a
11
+ * third caller needs it.
12
+ */
13
+ function mergeRefs(...refs) {
14
+ return (node) => {
15
+ for (const ref of refs) if (typeof ref === "function") ref(node);
16
+ else if (ref) ref.current = node;
17
+ };
18
+ }
19
+ //#endregion
20
+ export { mergeRefs };
21
+
22
+ //# sourceMappingURL=merge-refs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge-refs.js","names":[],"sources":["../../src/lib/merge-refs.ts"],"sourcesContent":["import type { Ref, RefCallback } from \"react\";\n\n/**\n * Combines a caller-supplied ref with a ref this component already needs for\n * its own purposes, so both receive the same node.\n *\n * Only two call sites need this -- Avatar's `measure` (image decode-state)\n * and ScrollArea's `region` (scrollTop reset) -- both already attached to the\n * same node the newly forwarded ref now also targets. A shared five-line\n * helper for two call sites is the Rule of Two; anything more general (a\n * `useMergedRef` hook, React 19 ref-cleanup support) is speculative until a\n * third caller needs it.\n */\nexport function mergeRefs<T>(...refs: (Ref<T> | undefined)[]): RefCallback<T> {\n return (node) => {\n for (const ref of refs) {\n if (typeof ref === \"function\") ref(node);\n else if (ref) (ref as { current: T | null }).current = node;\n }\n };\n}\n"],"mappings":";;;;;;;;;;;;AAaA,SAAgB,UAAa,GAAG,MAA8C;CAC5E,QAAQ,SAAS;EACf,KAAK,MAAM,OAAO,MAChB,IAAI,OAAO,QAAQ,YAAY,IAAI,IAAI;OAClC,IAAI,KAAK,IAA+B,UAAU;CAE3D;AACF"}
@@ -1,5 +1,5 @@
1
- import { MenuItemProps, MenuProps, MenuTrigger, PopoverProps } from "react-aria-components";
2
1
  import { ReactNode } from "react";
2
+ import { MenuItemProps, MenuProps, MenuTrigger, PopoverProps } from "react-aria-components";
3
3
  //#region src/menu/menu.d.ts
4
4
  /**
5
5
  * Menu, ported from the legacy shadcn/Radix `dropdown-menu` onto React
@@ -68,7 +68,7 @@ interface MenuProps$1 extends Omit<MenuProps<object>, "className" | "style" | "c
68
68
  /** Gap between the trigger and the menu. @default 4 */
69
69
  offset?: number;
70
70
  }
71
- declare function Menu$1({ children, className, placement, offset, ...props }: MenuProps$1): import("react").JSX.Element;
71
+ declare const Menu$1: import("react").ForwardRefExoticComponent<MenuProps$1 & import("react").RefAttributes<HTMLDivElement>>;
72
72
  /**
73
73
  * One menu row.
74
74
  *
@@ -80,7 +80,7 @@ interface MenuItemProps$1 extends Omit<MenuItemProps, "className" | "style"> {
80
80
  /** Extra classes. Merged last, so they win conflicts with ours. */
81
81
  className?: string;
82
82
  }
83
- declare function MenuItem$1({ className, ...props }: MenuItemProps$1): import("react").JSX.Element;
83
+ declare const MenuItem$1: import("react").ForwardRefExoticComponent<MenuItemProps$1 & import("react").RefAttributes<HTMLDivElement>>;
84
84
  /**
85
85
  * A divider between groups of items.
86
86
  *
@@ -90,9 +90,9 @@ declare function MenuItem$1({ className, ...props }: MenuItemProps$1): import("r
90
90
  * standalone, because `MenuInner` supplies `elementType: 'div'` -- an
91
91
  * `<hr>` is not valid inside a `role="menu"` container.
92
92
  */
93
- export declare function MenuSeparator({ className }: {
93
+ export declare const MenuSeparator: import("react").ForwardRefExoticComponent<{
94
94
  className?: string;
95
- }): import("react").JSX.Element;
95
+ } & import("react").RefAttributes<HTMLElement>>;
96
96
  //#endregion
97
97
  export { Menu$1 as Menu, MenuItem$1 as MenuItem, MenuItemProps$1 as MenuItemProps, MenuProps$1 as MenuProps, MenuTrigger };
98
98
  //# sourceMappingURL=menu.d.ts.map
package/dist/menu/menu.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { cn } from "../lib/cn.js";
3
3
  import { cva } from "class-variance-authority";
4
+ import { forwardRef } from "react";
4
5
  import { Menu, MenuItem, MenuTrigger, Popover, Separator } from "react-aria-components";
5
6
  import { jsx } from "react/jsx-runtime";
6
7
  //#region src/menu/menu.tsx
@@ -57,27 +58,29 @@ const menuItemVariants = cva([
57
58
  "[&_svg:not([class*='size-'])]:size-(--icon-sm)"
58
59
  ]);
59
60
  const menuSeparatorVariants = cva(["-mx-1 my-1 h-px bg-border border-none"]);
60
- function Menu$1({ children, className, placement = "bottom start", offset = 4, ...props }) {
61
+ const Menu$1 = forwardRef(function Menu$2({ children, className, placement = "bottom start", offset = 4, ...props }, ref) {
61
62
  return /* @__PURE__ */ jsx(Popover, {
62
63
  placement,
63
64
  offset,
64
65
  "data-slot": "menu-popover",
65
66
  className: cn(menuPopoverVariants(), className),
66
67
  children: /* @__PURE__ */ jsx(Menu, {
68
+ ref,
67
69
  ...props,
68
70
  "data-slot": "menu",
69
71
  className: menuVariants(),
70
72
  children
71
73
  })
72
74
  });
73
- }
74
- function MenuItem$1({ className, ...props }) {
75
+ });
76
+ const MenuItem$1 = forwardRef(function MenuItem$2({ className, ...props }, ref) {
75
77
  return /* @__PURE__ */ jsx(MenuItem, {
78
+ ref,
76
79
  ...props,
77
80
  "data-slot": "menu-item",
78
81
  className: cn(menuItemVariants(), className)
79
82
  });
80
- }
83
+ });
81
84
  /**
82
85
  * A divider between groups of items.
83
86
  *
@@ -87,12 +90,13 @@ function MenuItem$1({ className, ...props }) {
87
90
  * standalone, because `MenuInner` supplies `elementType: 'div'` -- an
88
91
  * `<hr>` is not valid inside a `role="menu"` container.
89
92
  */
90
- function MenuSeparator({ className }) {
93
+ const MenuSeparator = forwardRef(function MenuSeparator({ className }, ref) {
91
94
  return /* @__PURE__ */ jsx(Separator, {
95
+ ref,
92
96
  "data-slot": "menu-separator",
93
97
  className: cn(menuSeparatorVariants(), className)
94
98
  });
95
- }
99
+ });
96
100
  //#endregion
97
101
  export { Menu$1 as Menu, MenuItem$1 as MenuItem, MenuSeparator, MenuTrigger, menuItemVariants, menuPopoverVariants, menuSeparatorVariants, menuVariants };
98
102
 
@@ -1 +1 @@
1
- {"version":3,"file":"menu.js","names":["Menu","AriaPopover","AriaMenu","MenuItem","AriaMenuItem","AriaSeparator"],"sources":["../../src/menu/menu.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport type { ReactNode } from \"react\";\nimport {\n Menu as AriaMenu,\n MenuItem as AriaMenuItem,\n Popover as AriaPopover,\n Separator as AriaSeparator,\n type MenuItemProps as AriaMenuItemProps,\n type MenuProps as AriaMenuProps,\n type PopoverProps as AriaPopoverProps,\n} from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\n\n/**\n * Menu, ported from the legacy shadcn/Radix `dropdown-menu` onto React\n * Aria's Menu collection.\n *\n * FIFTEEN EXPORTS BECOME FOUR, and the census is unambiguous about it. Ten\n * files import the legacy component but only EIGHT render anything --\n * `header.tsx` and `PollQuizCreator.tsx` import parts and use none. Across\n * those eight: `DropdownMenuItem` 42, `DropdownMenu` 13,\n * `DropdownMenuTrigger` 13, `DropdownMenuContent` 13,\n * `DropdownMenuSeparator` 2.\n *\n * Everything else is ZERO: `Group`, `CheckboxItem`, `RadioGroup`,\n * `RadioItem`, `Label`, `Shortcut`, `Sub`, `SubTrigger`, `SubContent`,\n * `Portal`. Nine exports, no call sites. React Aria has `MenuSection`,\n * `Header` and selection modes ready if any of them turns out to be\n * wanted -- they are one component each, added the day something needs one.\n *\n * `variant=\"destructive\"` is also never passed, which resolves a real\n * problem rather than dodging one: `destructive-strong` on a `muted` hover\n * fill measures 4.29:1 against a 4.5 floor. The legacy has the same defect\n * unmeasured and worse (its `destructive/10` composite gives 4.01:1). With\n * no call site there is nothing to ship and nothing to weaken the contract\n * for.\n *\n * The surface is `card`, not a `popover` role: in the legacy theme\n * `--popover` is byte-identical to `--card` in both themes. Item hover is\n * `muted`, not `accent`: `--accent` is byte-identical to `--muted`. Two\n * more of the migration plan's proposed roles that already exist.\n *\n * Every string in the cva blocks below is a STATIC literal.\n */\nexport const menuPopoverVariants = cva([\n // min-w-32 is 8rem, the legacy min-w-[8rem] -- but as a spacing-scale\n // step rather than a bracket value that check:hardcoding rejects.\n \"min-w-32 overflow-y-auto\",\n \"rounded-md border-(length:--border-width) border-border p-1\",\n \"bg-card text-card-foreground shadow-floating\",\n \"box-border font-sans text-sm leading-(--leading-normal)\",\n]);\n\nexport const menuVariants = cva([\n \"flex flex-col outline-none\",\n]);\n\nexport const menuItemVariants = cva([\n \"relative flex cursor-pointer select-none items-center gap-2\",\n \"rounded-2xs px-2 py-1.5\",\n \"box-border text-sm leading-(--leading-normal) text-card-foreground\",\n \"outline-none\",\n\n // data-focused, NOT :hover. React Aria drives one highlight from both\n // pointer and keyboard, so the row under the mouse and the row the arrow\n // keys reached are the same row -- the legacy used `focus:` for the\n // keyboard and nothing for hover, so a mouse user saw no highlight at all\n // until the item took focus.\n \"data-[focused]:bg-muted\",\n\n // The focus ring goes on the ITEM, and it is an outline rather than a\n // background alone: a highlight made only of colour disappears under\n // forced-colors, and axe has no rule that would notice.\n \"data-[focus-visible]:outline-solid\",\n \"data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"data-[focus-visible]:outline-offset-(--focus-ring-offset)\",\n \"data-[focus-visible]:outline-(--color-ring)\",\n\n \"data-[disabled]:cursor-not-allowed data-[disabled]:opacity-(--opacity-disabled)\",\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0\",\n \"[&_svg:not([class*='size-'])]:size-(--icon-sm)\",\n]);\n\nexport const menuSeparatorVariants = cva([\"-mx-1 my-1 h-px bg-border border-none\"]);\n\n/**\n * A dropdown menu.\n *\n * @example\n * <MenuTrigger>\n * <Button>Actions</Button>\n * <Menu onAction={(key) => run(key)}>\n * <MenuItem id=\"edit\">Edit</MenuItem>\n * <MenuSeparator />\n * <MenuItem id=\"delete\">Delete</MenuItem>\n * </Menu>\n * </MenuTrigger>\n */\nexport interface MenuProps\n extends Omit<AriaMenuProps<object>, \"className\" | \"style\" | \"children\" | \"items\"> {\n /** `MenuItem` and `MenuSeparator` children. */\n children?: ReactNode;\n /** Extra classes for the popover. Merged last, so they win conflicts. */\n className?: string;\n /**\n * Where the menu opens, as React Aria's single `placement` string.\n *\n * The legacy split this across `side`, `align`, `sideOffset` and\n * `alignOffset`. Its census reads `align=\"end\"` 10 and `align=\"center\"`\n * 2, always with the default bottom side -- so `\"bottom end\"` covers ten\n * of the twelve.\n *\n * @default \"bottom start\"\n */\n placement?: AriaPopoverProps[\"placement\"];\n /** Gap between the trigger and the menu. @default 4 */\n offset?: number;\n}\n\nexport function Menu({\n children,\n className,\n placement = \"bottom start\",\n offset = 4,\n ...props\n}: MenuProps) {\n return (\n <AriaPopover\n placement={placement}\n offset={offset}\n data-slot=\"menu-popover\"\n className={cn(menuPopoverVariants(), className)}\n >\n <AriaMenu {...props} data-slot=\"menu\" className={menuVariants()}>\n {children}\n </AriaMenu>\n </AriaPopover>\n );\n}\n\n/**\n * One menu row.\n *\n * @example\n * <MenuItem id=\"edit\">Edit</MenuItem>\n * <MenuItem href=\"/settings\">Settings</MenuItem>\n */\nexport interface MenuItemProps extends Omit<AriaMenuItemProps, \"className\" | \"style\"> {\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nexport function MenuItem({ className, ...props }: MenuItemProps) {\n return (\n <AriaMenuItem {...props} data-slot=\"menu-item\" className={cn(menuItemVariants(), className)} />\n );\n}\n\n/**\n * A divider between groups of items.\n *\n * A real collection child: React Aria's `Separator` is a `SeparatorNode`,\n * so unlike an arbitrary element it is legal inside a Menu. Inside one it\n * renders `<div role=\"separator\">` rather than the `<hr>` it uses\n * standalone, because `MenuInner` supplies `elementType: 'div'` -- an\n * `<hr>` is not valid inside a `role=\"menu\"` container.\n */\nexport function MenuSeparator({ className }: { className?: string }) {\n return (\n <AriaSeparator\n data-slot=\"menu-separator\"\n className={cn(menuSeparatorVariants(), className)}\n />\n );\n}\n\n/**\n * Pairs a trigger with a menu and owns the open state.\n *\n * Re-exported from React Aria unchanged. It takes the trigger element\n * directly as its first child, which is what `asChild` was doing on all 13\n * legacy call sites.\n */\nexport { MenuTrigger } from \"react-aria-components\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+CA,MAAa,sBAAsB,IAAI;CAGrC;CACA;CACA;CACA;AACF,CAAC;AAED,MAAa,eAAe,IAAI,CAC9B,4BACF,CAAC;AAED,MAAa,mBAAmB,IAAI;CAClC;CACA;CACA;CACA;CAOA;CAKA;CACA;CACA;CACA;CAEA;CACA;CACA;AACF,CAAC;AAED,MAAa,wBAAwB,IAAI,CAAC,uCAAuC,CAAC;AAoClF,SAAgBA,OAAK,EACnB,UACA,WACA,YAAY,gBACZ,SAAS,GACT,GAAG,SACS;CACZ,OACE,oBAACC,SAAD;EACa;EACH;EACR,aAAU;EACV,WAAW,GAAG,oBAAoB,GAAG,SAAS;EAE9C,UAAA,oBAACC,MAAD;GAAU,GAAI;GAAO,aAAU;GAAO,WAAW,aAAa;GAC3D;EACO,CAAA;CACC,CAAA;AAEjB;AAcA,SAAgBC,WAAS,EAAE,WAAW,GAAG,SAAwB;CAC/D,OACE,oBAACC,UAAD;EAAc,GAAI;EAAO,aAAU;EAAY,WAAW,GAAG,iBAAiB,GAAG,SAAS;CAAI,CAAA;AAElG;;;;;;;;;;AAWA,SAAgB,cAAc,EAAE,aAAqC;CACnE,OACE,oBAACC,WAAD;EACE,aAAU;EACV,WAAW,GAAG,sBAAsB,GAAG,SAAS;CACjD,CAAA;AAEL"}
1
+ {"version":3,"file":"menu.js","names":["Menu","AriaPopover","AriaMenu","MenuItem","AriaMenuItem","AriaSeparator"],"sources":["../../src/menu/menu.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, type ReactNode } from \"react\";\nimport {\n Menu as AriaMenu,\n MenuItem as AriaMenuItem,\n Popover as AriaPopover,\n Separator as AriaSeparator,\n type MenuItemProps as AriaMenuItemProps,\n type MenuProps as AriaMenuProps,\n type PopoverProps as AriaPopoverProps,\n} from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\n\n/**\n * Menu, ported from the legacy shadcn/Radix `dropdown-menu` onto React\n * Aria's Menu collection.\n *\n * FIFTEEN EXPORTS BECOME FOUR, and the census is unambiguous about it. Ten\n * files import the legacy component but only EIGHT render anything --\n * `header.tsx` and `PollQuizCreator.tsx` import parts and use none. Across\n * those eight: `DropdownMenuItem` 42, `DropdownMenu` 13,\n * `DropdownMenuTrigger` 13, `DropdownMenuContent` 13,\n * `DropdownMenuSeparator` 2.\n *\n * Everything else is ZERO: `Group`, `CheckboxItem`, `RadioGroup`,\n * `RadioItem`, `Label`, `Shortcut`, `Sub`, `SubTrigger`, `SubContent`,\n * `Portal`. Nine exports, no call sites. React Aria has `MenuSection`,\n * `Header` and selection modes ready if any of them turns out to be\n * wanted -- they are one component each, added the day something needs one.\n *\n * `variant=\"destructive\"` is also never passed, which resolves a real\n * problem rather than dodging one: `destructive-strong` on a `muted` hover\n * fill measures 4.29:1 against a 4.5 floor. The legacy has the same defect\n * unmeasured and worse (its `destructive/10` composite gives 4.01:1). With\n * no call site there is nothing to ship and nothing to weaken the contract\n * for.\n *\n * The surface is `card`, not a `popover` role: in the legacy theme\n * `--popover` is byte-identical to `--card` in both themes. Item hover is\n * `muted`, not `accent`: `--accent` is byte-identical to `--muted`. Two\n * more of the migration plan's proposed roles that already exist.\n *\n * Every string in the cva blocks below is a STATIC literal.\n */\nexport const menuPopoverVariants = cva([\n // min-w-32 is 8rem, the legacy min-w-[8rem] -- but as a spacing-scale\n // step rather than a bracket value that check:hardcoding rejects.\n \"min-w-32 overflow-y-auto\",\n \"rounded-md border-(length:--border-width) border-border p-1\",\n \"bg-card text-card-foreground shadow-floating\",\n \"box-border font-sans text-sm leading-(--leading-normal)\",\n]);\n\nexport const menuVariants = cva([\n \"flex flex-col outline-none\",\n]);\n\nexport const menuItemVariants = cva([\n \"relative flex cursor-pointer select-none items-center gap-2\",\n \"rounded-2xs px-2 py-1.5\",\n \"box-border text-sm leading-(--leading-normal) text-card-foreground\",\n \"outline-none\",\n\n // data-focused, NOT :hover. React Aria drives one highlight from both\n // pointer and keyboard, so the row under the mouse and the row the arrow\n // keys reached are the same row -- the legacy used `focus:` for the\n // keyboard and nothing for hover, so a mouse user saw no highlight at all\n // until the item took focus.\n \"data-[focused]:bg-muted\",\n\n // The focus ring goes on the ITEM, and it is an outline rather than a\n // background alone: a highlight made only of colour disappears under\n // forced-colors, and axe has no rule that would notice.\n \"data-[focus-visible]:outline-solid\",\n \"data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"data-[focus-visible]:outline-offset-(--focus-ring-offset)\",\n \"data-[focus-visible]:outline-(--color-ring)\",\n\n \"data-[disabled]:cursor-not-allowed data-[disabled]:opacity-(--opacity-disabled)\",\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0\",\n \"[&_svg:not([class*='size-'])]:size-(--icon-sm)\",\n]);\n\nexport const menuSeparatorVariants = cva([\"-mx-1 my-1 h-px bg-border border-none\"]);\n\n/**\n * A dropdown menu.\n *\n * @example\n * <MenuTrigger>\n * <Button>Actions</Button>\n * <Menu onAction={(key) => run(key)}>\n * <MenuItem id=\"edit\">Edit</MenuItem>\n * <MenuSeparator />\n * <MenuItem id=\"delete\">Delete</MenuItem>\n * </Menu>\n * </MenuTrigger>\n */\nexport interface MenuProps\n extends Omit<AriaMenuProps<object>, \"className\" | \"style\" | \"children\" | \"items\"> {\n /** `MenuItem` and `MenuSeparator` children. */\n children?: ReactNode;\n /** Extra classes for the popover. Merged last, so they win conflicts. */\n className?: string;\n /**\n * Where the menu opens, as React Aria's single `placement` string.\n *\n * The legacy split this across `side`, `align`, `sideOffset` and\n * `alignOffset`. Its census reads `align=\"end\"` 10 and `align=\"center\"`\n * 2, always with the default bottom side -- so `\"bottom end\"` covers ten\n * of the twelve.\n *\n * @default \"bottom start\"\n */\n placement?: AriaPopoverProps[\"placement\"];\n /** Gap between the trigger and the menu. @default 4 */\n offset?: number;\n}\n\nexport const Menu = forwardRef<HTMLDivElement, MenuProps>(function Menu(\n { children, className, placement = \"bottom start\", offset = 4, ...props },\n ref,\n) {\n return (\n <AriaPopover\n placement={placement}\n offset={offset}\n data-slot=\"menu-popover\"\n className={cn(menuPopoverVariants(), className)}\n >\n <AriaMenu\n // The ref goes on the MENU, not the POPOVER above. The popover here\n // is purely a positioner -- same role as an overlay wrapper -- and\n // the props spread and children already land on AriaMenu.\n ref={ref}\n {...props}\n data-slot=\"menu\"\n className={menuVariants()}\n >\n {children}\n </AriaMenu>\n </AriaPopover>\n );\n});\n\n/**\n * One menu row.\n *\n * @example\n * <MenuItem id=\"edit\">Edit</MenuItem>\n * <MenuItem href=\"/settings\">Settings</MenuItem>\n */\nexport interface MenuItemProps extends Omit<AriaMenuItemProps, \"className\" | \"style\"> {\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nexport const MenuItem = forwardRef<HTMLDivElement, MenuItemProps>(function MenuItem(\n { className, ...props },\n ref,\n) {\n return (\n <AriaMenuItem\n ref={ref}\n {...props}\n data-slot=\"menu-item\"\n className={cn(menuItemVariants(), className)}\n />\n );\n});\n\n/**\n * A divider between groups of items.\n *\n * A real collection child: React Aria's `Separator` is a `SeparatorNode`,\n * so unlike an arbitrary element it is legal inside a Menu. Inside one it\n * renders `<div role=\"separator\">` rather than the `<hr>` it uses\n * standalone, because `MenuInner` supplies `elementType: 'div'` -- an\n * `<hr>` is not valid inside a `role=\"menu\"` container.\n */\nexport const MenuSeparator = forwardRef<HTMLElement, { className?: string }>(\n function MenuSeparator({ className }, ref) {\n return (\n <AriaSeparator\n ref={ref}\n data-slot=\"menu-separator\"\n className={cn(menuSeparatorVariants(), className)}\n />\n );\n },\n);\n\n/**\n * Pairs a trigger with a menu and owns the open state.\n *\n * Re-exported from React Aria unchanged. It takes the trigger element\n * directly as its first child, which is what `asChild` was doing on all 13\n * legacy call sites.\n */\nexport { MenuTrigger } from \"react-aria-components\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+CA,MAAa,sBAAsB,IAAI;CAGrC;CACA;CACA;CACA;AACF,CAAC;AAED,MAAa,eAAe,IAAI,CAC9B,4BACF,CAAC;AAED,MAAa,mBAAmB,IAAI;CAClC;CACA;CACA;CACA;CAOA;CAKA;CACA;CACA;CACA;CAEA;CACA;CACA;AACF,CAAC;AAED,MAAa,wBAAwB,IAAI,CAAC,uCAAuC,CAAC;AAoClF,MAAaA,SAAO,WAAsC,SAASA,OACjE,EAAE,UAAU,WAAW,YAAY,gBAAgB,SAAS,GAAG,GAAG,SAClE,KACA;CACA,OACE,oBAACC,SAAD;EACa;EACH;EACR,aAAU;EACV,WAAW,GAAG,oBAAoB,GAAG,SAAS;EAE9C,UAAA,oBAACC,MAAD;GAIO;GACL,GAAI;GACJ,aAAU;GACV,WAAW,aAAa;GAEvB;EACO,CAAA;CACC,CAAA;AAEjB,CAAC;AAcD,MAAaC,aAAW,WAA0C,SAASA,WACzE,EAAE,WAAW,GAAG,SAChB,KACA;CACA,OACE,oBAACC,UAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,iBAAiB,GAAG,SAAS;CAC5C,CAAA;AAEL,CAAC;;;;;;;;;;AAWD,MAAa,gBAAgB,WAC3B,SAAS,cAAc,EAAE,aAAa,KAAK;CACzC,OACE,oBAACC,WAAD;EACO;EACL,aAAU;EACV,WAAW,GAAG,sBAAsB,GAAG,SAAS;CACjD,CAAA;AAEL,CACF"}
@@ -1,5 +1,5 @@
1
- import { DialogTrigger as PopoverTrigger, PopoverProps } from "react-aria-components";
2
1
  import { ReactNode } from "react";
2
+ import { DialogTrigger as PopoverTrigger, PopoverProps } from "react-aria-components";
3
3
  //#region src/popover/popover.d.ts
4
4
  /**
5
5
  * Popover, ported from the legacy shadcn/Radix component.
@@ -44,7 +44,7 @@ interface PopoverProps$1 extends Omit<PopoverProps, "className" | "style" | "chi
44
44
  */
45
45
  placement?: PopoverProps["placement"];
46
46
  }
47
- declare function Popover$1({ children, className, placement, offset, ...props }: PopoverProps$1): import("react").JSX.Element;
47
+ declare const Popover$1: import("react").ForwardRefExoticComponent<PopoverProps$1 & import("react").RefAttributes<HTMLElement>>;
48
48
  //#endregion
49
49
  export { Popover$1 as Popover, PopoverProps$1 as PopoverProps, PopoverTrigger };
50
50
  //# sourceMappingURL=popover.d.ts.map
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { cn } from "../lib/cn.js";
3
3
  import { cva } from "class-variance-authority";
4
+ import { forwardRef } from "react";
4
5
  import { DialogTrigger as PopoverTrigger, Popover } from "react-aria-components";
5
6
  import { jsx } from "react/jsx-runtime";
6
7
  //#region src/popover/popover.tsx
@@ -30,8 +31,9 @@ const popoverVariants = cva([
30
31
  "box-border font-sans text-sm leading-(--leading-normal)",
31
32
  "outline-none"
32
33
  ]);
33
- function Popover$1({ children, className, placement = "bottom", offset = 4, ...props }) {
34
+ const Popover$1 = forwardRef(function Popover$2({ children, className, placement = "bottom", offset = 4, ...props }, ref) {
34
35
  return /* @__PURE__ */ jsx(Popover, {
36
+ ref,
35
37
  ...props,
36
38
  placement,
37
39
  offset,
@@ -39,7 +41,7 @@ function Popover$1({ children, className, placement = "bottom", offset = 4, ...p
39
41
  className: cn(popoverVariants(), className),
40
42
  children
41
43
  });
42
- }
44
+ });
43
45
  //#endregion
44
46
  export { Popover$1 as Popover, PopoverTrigger, popoverVariants };
45
47
 
@@ -1 +1 @@
1
- {"version":3,"file":"popover.js","names":["Popover","AriaPopover"],"sources":["../../src/popover/popover.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport type { ReactNode } from \"react\";\nimport {\n Popover as AriaPopover,\n type PopoverProps as AriaPopoverProps,\n} from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\n\n/**\n * Popover, ported from the legacy shadcn/Radix component.\n *\n * Census: 2 files, 2 uses. `asChild` on the trigger twice, `className`\n * twice, `align=\"start\"` once. `PopoverAnchor` is exported by the legacy\n * and imported by nobody, so it is not ported; neither are `side`,\n * `sideOffset`, `alignOffset`, `open` / `onOpenChange`, `modal`,\n * `collisionPadding` or `avoidCollisions` -- zero call sites each. Both\n * existing uses are uncontrolled.\n *\n * IT GAINS AN ACCESSIBLE NAME. The legacy renders `role=\"dialog\"` with no\n * `aria-label` and no `aria-labelledby`, so both call sites announce an\n * unnamed dialog. React Aria labels the popover from its trigger\n * automatically once the two are paired, which is the only way to build\n * one here.\n *\n * The surface is `card`: in the legacy theme `--popover` is byte-identical\n * to `--card` in both themes, so the role the migration plan proposed\n * already exists under another name.\n */\nexport const popoverVariants = cva([\n // w-72 (288px) is the legacy default width, as a spacing-scale step.\n \"w-72 rounded-md border-(length:--border-width) border-border p-4\",\n \"bg-card text-card-foreground shadow-floating\",\n // The reset in @seekalfred/styles is OPT-IN, so a consumer may load\n // none: an unstyled document renders text in a serif, and content-box\n // would add the 16px padding on top of the width.\n \"box-border font-sans text-sm leading-(--leading-normal)\",\n // React Aria gives the popover tabindex=-1 and focuses it on open, so it\n // can take a focus ring it does not want.\n \"outline-none\",\n]);\n\n/**\n * Content anchored to a trigger.\n *\n * @example\n * <PopoverTrigger>\n * <Button>Pick a date</Button>\n * <Popover placement=\"bottom start\">...</Popover>\n * </PopoverTrigger>\n */\nexport interface PopoverProps\n extends Omit<AriaPopoverProps, \"className\" | \"style\" | \"children\"> {\n children?: ReactNode;\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n /**\n * Where it opens, as React Aria's single `placement` string. The legacy\n * split this across `side` and `align`; its one non-default call site is\n * `align=\"start\"`, which is `\"bottom start\"` here.\n *\n * @default \"bottom\"\n */\n placement?: AriaPopoverProps[\"placement\"];\n}\n\nexport function Popover({\n children,\n className,\n placement = \"bottom\",\n offset = 4,\n ...props\n}: PopoverProps) {\n return (\n <AriaPopover\n {...props}\n placement={placement}\n offset={offset}\n data-slot=\"popover\"\n className={cn(popoverVariants(), className)}\n >\n {/*\n No inner Dialog. Wrapping the content in one is the common React\n Aria idiom and it was in the first draft here -- but deleting it\n broke no assertion, so it was doing nothing: a Popover under a\n DialogTrigger already gets role=\"dialog\", a focus scope, and\n aria-labelledby pointing at the trigger. The wrapper was one more\n element and one more className to explain.\n */}\n {children}\n </AriaPopover>\n );\n}\n\n/**\n * Pairs a trigger with a popover and owns the open state.\n *\n * React Aria's `DialogTrigger`, renamed for the migration: it takes the\n * trigger element directly as its first child, which is what `asChild` was\n * doing on both legacy call sites.\n */\nexport { DialogTrigger as PopoverTrigger } from \"react-aria-components\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,MAAa,kBAAkB,IAAI;CAEjC;CACA;CAIA;CAGA;AACF,CAAC;AA0BD,SAAgBA,UAAQ,EACtB,UACA,WACA,YAAY,UACZ,SAAS,GACT,GAAG,SACY;CACf,OACE,oBAACC,SAAD;EACE,GAAI;EACO;EACH;EACR,aAAU;EACV,WAAW,GAAG,gBAAgB,GAAG,SAAS;EAUzC;CACU,CAAA;AAEjB"}
1
+ {"version":3,"file":"popover.js","names":["Popover","AriaPopover"],"sources":["../../src/popover/popover.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, type ReactNode } from \"react\";\nimport {\n Popover as AriaPopover,\n type PopoverProps as AriaPopoverProps,\n} from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\n\n/**\n * Popover, ported from the legacy shadcn/Radix component.\n *\n * Census: 2 files, 2 uses. `asChild` on the trigger twice, `className`\n * twice, `align=\"start\"` once. `PopoverAnchor` is exported by the legacy\n * and imported by nobody, so it is not ported; neither are `side`,\n * `sideOffset`, `alignOffset`, `open` / `onOpenChange`, `modal`,\n * `collisionPadding` or `avoidCollisions` -- zero call sites each. Both\n * existing uses are uncontrolled.\n *\n * IT GAINS AN ACCESSIBLE NAME. The legacy renders `role=\"dialog\"` with no\n * `aria-label` and no `aria-labelledby`, so both call sites announce an\n * unnamed dialog. React Aria labels the popover from its trigger\n * automatically once the two are paired, which is the only way to build\n * one here.\n *\n * The surface is `card`: in the legacy theme `--popover` is byte-identical\n * to `--card` in both themes, so the role the migration plan proposed\n * already exists under another name.\n */\nexport const popoverVariants = cva([\n // w-72 (288px) is the legacy default width, as a spacing-scale step.\n \"w-72 rounded-md border-(length:--border-width) border-border p-4\",\n \"bg-card text-card-foreground shadow-floating\",\n // The reset in @seekalfred/styles is OPT-IN, so a consumer may load\n // none: an unstyled document renders text in a serif, and content-box\n // would add the 16px padding on top of the width.\n \"box-border font-sans text-sm leading-(--leading-normal)\",\n // React Aria gives the popover tabindex=-1 and focuses it on open, so it\n // can take a focus ring it does not want.\n \"outline-none\",\n]);\n\n/**\n * Content anchored to a trigger.\n *\n * @example\n * <PopoverTrigger>\n * <Button>Pick a date</Button>\n * <Popover placement=\"bottom start\">...</Popover>\n * </PopoverTrigger>\n */\nexport interface PopoverProps\n extends Omit<AriaPopoverProps, \"className\" | \"style\" | \"children\"> {\n children?: ReactNode;\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n /**\n * Where it opens, as React Aria's single `placement` string. The legacy\n * split this across `side` and `align`; its one non-default call site is\n * `align=\"start\"`, which is `\"bottom start\"` here.\n *\n * @default \"bottom\"\n */\n placement?: AriaPopoverProps[\"placement\"];\n}\n\nexport const Popover = forwardRef<HTMLElement, PopoverProps>(function Popover(\n { children, className, placement = \"bottom\", offset = 4, ...props },\n ref,\n) {\n return (\n <AriaPopover\n ref={ref}\n {...props}\n placement={placement}\n offset={offset}\n data-slot=\"popover\"\n className={cn(popoverVariants(), className)}\n >\n {/*\n No inner Dialog. Wrapping the content in one is the common React\n Aria idiom and it was in the first draft here -- but deleting it\n broke no assertion, so it was doing nothing: a Popover under a\n DialogTrigger already gets role=\"dialog\", a focus scope, and\n aria-labelledby pointing at the trigger. The wrapper was one more\n element and one more className to explain.\n */}\n {children}\n </AriaPopover>\n );\n});\n\n/**\n * Pairs a trigger with a popover and owns the open state.\n *\n * React Aria's `DialogTrigger`, renamed for the migration: it takes the\n * trigger element directly as its first child, which is what `asChild` was\n * doing on both legacy call sites.\n */\nexport { DialogTrigger as PopoverTrigger } from \"react-aria-components\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,MAAa,kBAAkB,IAAI;CAEjC;CACA;CAIA;CAGA;AACF,CAAC;AA0BD,MAAaA,YAAU,WAAsC,SAASA,UACpE,EAAE,UAAU,WAAW,YAAY,UAAU,SAAS,GAAG,GAAG,SAC5D,KACA;CACA,OACE,oBAACC,SAAD;EACO;EACL,GAAI;EACO;EACH;EACR,aAAU;EACV,WAAW,GAAG,gBAAgB,GAAG,SAAS;EAUzC;CACU,CAAA;AAEjB,CAAC"}
@@ -1,6 +1,6 @@
1
1
  import { VariantProps } from "class-variance-authority";
2
- import { ProgressBarProps } from "react-aria-components";
3
2
  import { ReactNode } from "react";
3
+ import { ProgressBarProps } from "react-aria-components";
4
4
  //#region src/progress/progress.d.ts
5
5
  /**
6
6
  * Progress, ported from the legacy shadcn/Radix component.
@@ -77,6 +77,6 @@ export interface ProgressProps extends Omit<ProgressBarProps, "className" | "sty
77
77
  /** Extra classes. Merged last, so they win conflicts with ours. */
78
78
  className?: string;
79
79
  }
80
- export declare function Progress({ label, labelHidden, showValue, tone, trackClassName, fillClassName, className, ...props }: ProgressProps): import("react").JSX.Element;
80
+ export declare const Progress: import("react").ForwardRefExoticComponent<ProgressProps & import("react").RefAttributes<HTMLDivElement>>;
81
81
  //#endregion
82
82
  //# sourceMappingURL=progress.d.ts.map
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { cn, cnAll } from "../lib/cn.js";
3
3
  import { cva } from "class-variance-authority";
4
+ import { forwardRef } from "react";
4
5
  import { Label, ProgressBar } from "react-aria-components";
5
6
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
7
  //#region src/progress/progress.tsx
@@ -46,8 +47,9 @@ const progressFillVariants = cva(["h-full rounded-pill", "motion-safe:transition
46
47
  } },
47
48
  defaultVariants: { tone: "neutral" }
48
49
  });
49
- function Progress({ label, labelHidden, showValue, tone, trackClassName, fillClassName, className, ...props }) {
50
+ const Progress = forwardRef(function Progress({ label, labelHidden, showValue, tone, trackClassName, fillClassName, className, ...props }, ref) {
50
51
  return /* @__PURE__ */ jsx(ProgressBar, {
52
+ ref,
51
53
  ...props,
52
54
  "data-slot": "progress",
53
55
  className: cn(progressVariants(), className),
@@ -75,7 +77,7 @@ function Progress({ label, labelHidden, showValue, tone, trackClassName, fillCla
75
77
  })
76
78
  })] })
77
79
  });
78
- }
80
+ });
79
81
  //#endregion
80
82
  export { Progress, progressFillVariants, progressTrackVariants, progressVariants };
81
83
 
@@ -1 +1 @@
1
- {"version":3,"file":"progress.js","names":["AriaProgressBar","AriaLabel"],"sources":["../../src/progress/progress.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type { ReactNode } from \"react\";\nimport {\n Label as AriaLabel,\n ProgressBar as AriaProgressBar,\n type ProgressBarProps as AriaProgressBarProps,\n} from \"react-aria-components\";\n\nimport { cn, cnAll } from \"../lib/cn.js\";\n\n/**\n * Progress, ported from the legacy shadcn/Radix component.\n *\n * Census: seven files import it and only TWO render anything --\n * `ResponseOption.tsx` and `LiveQA.tsx`. Both pass `value` and\n * `className` and nothing else.\n *\n * `label` IS REQUIRED, and that is the point. Neither live call site\n * passes `aria-label`, `aria-labelledby` or an `id` a label targets, and\n * `role=\"progressbar\"` does not take its name from content -- so both\n * rendered progress bars are announced as an unnamed progressbar. React\n * Aria supplies the plumbing for three naming routes but no name of its\n * own: `<ProgressBar value={60}/>` is just as nameless there. Making the\n * prop required is what turns it into something you cannot forget.\n *\n * The migration plan said this component \"adds the missing\n * aria-valuenow\". That is wrong -- Radix already supplies `aria-valuenow`,\n * `aria-valuemin`, `aria-valuemax` and `aria-valuetext`. The missing thing\n * was always the NAME.\n *\n * THE COLOURS ARE NOT THE LEGACY'S. Its track is `bg-primary/20`, an alpha\n * fill that composites against unknown page content and therefore has no\n * fixed contrast pair at all -- over white it is roughly 1.05:1, an\n * invisible boundary. Its indicator is `bg-primary`, 2.44:1 on the page.\n * Here the track is `muted` with a `border` boundary and the fill is\n * `primary-strong`, which puts the fill against the track at 4.61:1 and\n * the track against the page at 5.25:1.\n *\n * Both are in the build-failing contrast contract.\n */\nexport const progressVariants = cva([\"flex w-full flex-col gap-1.5 box-border font-sans\"]);\n\nexport const progressTrackVariants = cva([\n \"relative h-2 w-full overflow-hidden rounded-pill box-border\",\n // muted, NOT `border` as the Switch uses. A switch has no fill; a\n // progress bar does, and primary-strong on a `border` track is 1.01:1 --\n // the two halves are the same grey and the value disappears.\n \"bg-muted border-(length:--border-width) border-border\",\n]);\n\nexport const progressFillVariants = cva(\n [\n \"h-full rounded-pill\",\n \"motion-safe:transition-[width] motion-safe:duration-(--duration-base) motion-safe:ease-standard\",\n ],\n {\n variants: {\n /*\n * What the value MEANS, which for a progress bar is usually a score\n * rather than a completion. The product's bar carried exactly this axis\n * (`default`/`success`/`warning`/`error`/`info`) and reached for it to\n * colour performance metrics.\n *\n * `-strong` throughout: the fill sits on the `muted` track and has to\n * clear the 3:1 non-text floor of WCAG 1.4.11 against it, which is the\n * same reason the track is muted rather than `border` -- primary-strong\n * on a `border` track measures 1.01:1 and the value disappears.\n */\n tone: {\n neutral: \"bg-primary-strong\",\n success: \"bg-success-strong\",\n warning: \"bg-warning-strong\",\n danger: \"bg-destructive-strong\",\n info: \"bg-info-strong\",\n },\n },\n defaultVariants: { tone: \"neutral\" },\n },\n);\n\n/**\n * A determinate or indeterminate progress bar.\n *\n * @example\n * <Progress label=\"Upload\" value={62} />\n * <Progress label=\"Loading results\" />\n */\nexport interface ProgressProps\n extends Omit<AriaProgressBarProps, \"className\" | \"style\" | \"children\">,\n VariantProps<typeof progressFillVariants> {\n /**\n * What is progressing, and the bar's accessible name.\n *\n * **Required.** Both legacy call sites render an unnamed progressbar.\n * Pass `labelHidden` when the surrounding UI already says it.\n */\n label: ReactNode;\n /**\n * Render the label for screen readers only.\n *\n * Both legacy call sites sit beside visible text that was never\n * associated with the bar -- this is how that text becomes the name\n * without printing it twice.\n */\n labelHidden?: boolean;\n /**\n * Print the value beside the label.\n *\n * The text itself comes from React Aria, which already formats it from\n * `value`/`minValue`/`maxValue` and honours `formatOptions` and\n * `valueLabel` -- so a percentage, a currency or \"3 of 7\" are all reachable\n * without a second prop here. It was never RENDERED before: the bar\n * announced its value to assistive tech and showed a sighted reader nothing.\n */\n showValue?: boolean;\n /** Extra classes for the track. */\n trackClassName?: string;\n /** Extra classes for the fill -- a per-call-site colour the tone axis cannot express. */\n fillClassName?: string;\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nexport function Progress({\n label,\n labelHidden,\n showValue,\n tone,\n trackClassName,\n fillClassName,\n className,\n ...props\n}: ProgressProps) {\n return (\n <AriaProgressBar\n {...props}\n data-slot=\"progress\"\n className={cn(progressVariants(), className)}\n >\n {({ percentage, isIndeterminate, valueText }) => (\n <>\n {/*\n The row exists only when the value is printed. Wrapping the label\n in a flex row unconditionally would change the label's box for\n every existing consumer to no purpose.\n */}\n <div\n data-slot=\"progress-header\"\n className={cnAll(showValue && \"flex items-baseline justify-between gap-2\")}\n >\n <AriaLabel\n data-slot=\"progress-label\"\n className={cnAll(\n \"text-sm leading-(--leading-normal) text-foreground\",\n labelHidden && \"sr-only\",\n )}\n >\n {label}\n </AriaLabel>\n {showValue ? (\n <span\n data-slot=\"progress-value\"\n // aria-hidden: React Aria already announces the value on the\n // progressbar itself. Without this a screen reader reads it\n // twice, once as the widget's value and once as loose text.\n aria-hidden=\"true\"\n className=\"text-sm leading-(--leading-normal) text-muted-foreground tabular-nums\"\n >\n {valueText}\n </span>\n ) : null}\n </div>\n <div\n data-slot=\"progress-track\"\n className={cn(progressTrackVariants(), trackClassName)}\n >\n {/*\n width, not the legacy translateX(-(100 - value)%). That\n overflowed for value > 100 -- a positive translate slid the\n indicator off to the right, while the announced value stayed\n whatever was passed. React Aria clamps the value, so the two\n can no longer disagree.\n\n An indeterminate bar renders no fill rather than the\n legacy's silent 0%: `value={undefined}` became `(value || 0)`\n there, so a sighted user saw \"0% done\" while a screen reader\n said \"indeterminate\".\n */}\n <div\n data-slot=\"progress-fill\"\n data-indeterminate={isIndeterminate || undefined}\n className={cn(progressFillVariants({ tone }), fillClassName)}\n style={{ width: isIndeterminate ? \"0%\" : `${percentage ?? 0}%` }}\n />\n </div>\n </>\n )}\n </AriaProgressBar>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,MAAa,mBAAmB,IAAI,CAAC,mDAAmD,CAAC;AAEzF,MAAa,wBAAwB,IAAI,CACvC,+DAIA,uDACF,CAAC;AAED,MAAa,uBAAuB,IAClC,CACE,uBACA,iGACF,GACA;CACE,UAAU,EAYR,MAAM;EACJ,SAAS;EACT,SAAS;EACT,SAAS;EACT,QAAQ;EACR,MAAM;CACR,EACF;CACA,iBAAiB,EAAE,MAAM,UAAU;AACrC,CACF;AA6CA,SAAgB,SAAS,EACvB,OACA,aACA,WACA,MACA,gBACA,eACA,WACA,GAAG,SACa;CAChB,OACE,oBAACA,aAAD;EACE,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,iBAAiB,GAAG,SAAS;EAEzC,WAAA,EAAE,YAAY,iBAAiB,gBAC/B,qBAAA,UAAA,EAAA,UAAA,CAME,qBAAC,OAAD;GACE,aAAU;GACV,WAAW,MAAM,aAAa,2CAA2C;GAF3E,UAAA,CAIE,oBAACC,OAAD;IACE,aAAU;IACV,WAAW,MACT,sDACA,eAAe,SACjB;IAEC,UAAA;GACQ,CAAA,GACV,YACC,oBAAC,QAAD;IACE,aAAU;IAIV,eAAY;IACZ,WAAU;IAET,UAAA;GACG,CAAA,IACJ,IACD;EACL,CAAA,GAAA,oBAAC,OAAD;GACE,aAAU;GACV,WAAW,GAAG,sBAAsB,GAAG,cAAc;GAcrD,UAAA,oBAAC,OAAD;IACE,aAAU;IACV,sBAAoB,mBAAmB,KAAA;IACvC,WAAW,GAAG,qBAAqB,EAAE,KAAK,CAAC,GAAG,aAAa;IAC3D,OAAO,EAAE,OAAO,kBAAkB,OAAO,GAAG,cAAc,EAAE,GAAG;GAChE,CAAA;EACE,CAAA,CACL,EAAA,CAAA;CAEW,CAAA;AAErB"}
1
+ {"version":3,"file":"progress.js","names":["AriaProgressBar","AriaLabel"],"sources":["../../src/progress/progress.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { forwardRef, type ReactNode } from \"react\";\nimport {\n Label as AriaLabel,\n ProgressBar as AriaProgressBar,\n type ProgressBarProps as AriaProgressBarProps,\n} from \"react-aria-components\";\n\nimport { cn, cnAll } from \"../lib/cn.js\";\n\n/**\n * Progress, ported from the legacy shadcn/Radix component.\n *\n * Census: seven files import it and only TWO render anything --\n * `ResponseOption.tsx` and `LiveQA.tsx`. Both pass `value` and\n * `className` and nothing else.\n *\n * `label` IS REQUIRED, and that is the point. Neither live call site\n * passes `aria-label`, `aria-labelledby` or an `id` a label targets, and\n * `role=\"progressbar\"` does not take its name from content -- so both\n * rendered progress bars are announced as an unnamed progressbar. React\n * Aria supplies the plumbing for three naming routes but no name of its\n * own: `<ProgressBar value={60}/>` is just as nameless there. Making the\n * prop required is what turns it into something you cannot forget.\n *\n * The migration plan said this component \"adds the missing\n * aria-valuenow\". That is wrong -- Radix already supplies `aria-valuenow`,\n * `aria-valuemin`, `aria-valuemax` and `aria-valuetext`. The missing thing\n * was always the NAME.\n *\n * THE COLOURS ARE NOT THE LEGACY'S. Its track is `bg-primary/20`, an alpha\n * fill that composites against unknown page content and therefore has no\n * fixed contrast pair at all -- over white it is roughly 1.05:1, an\n * invisible boundary. Its indicator is `bg-primary`, 2.44:1 on the page.\n * Here the track is `muted` with a `border` boundary and the fill is\n * `primary-strong`, which puts the fill against the track at 4.61:1 and\n * the track against the page at 5.25:1.\n *\n * Both are in the build-failing contrast contract.\n */\nexport const progressVariants = cva([\"flex w-full flex-col gap-1.5 box-border font-sans\"]);\n\nexport const progressTrackVariants = cva([\n \"relative h-2 w-full overflow-hidden rounded-pill box-border\",\n // muted, NOT `border` as the Switch uses. A switch has no fill; a\n // progress bar does, and primary-strong on a `border` track is 1.01:1 --\n // the two halves are the same grey and the value disappears.\n \"bg-muted border-(length:--border-width) border-border\",\n]);\n\nexport const progressFillVariants = cva(\n [\n \"h-full rounded-pill\",\n \"motion-safe:transition-[width] motion-safe:duration-(--duration-base) motion-safe:ease-standard\",\n ],\n {\n variants: {\n /*\n * What the value MEANS, which for a progress bar is usually a score\n * rather than a completion. The product's bar carried exactly this axis\n * (`default`/`success`/`warning`/`error`/`info`) and reached for it to\n * colour performance metrics.\n *\n * `-strong` throughout: the fill sits on the `muted` track and has to\n * clear the 3:1 non-text floor of WCAG 1.4.11 against it, which is the\n * same reason the track is muted rather than `border` -- primary-strong\n * on a `border` track measures 1.01:1 and the value disappears.\n */\n tone: {\n neutral: \"bg-primary-strong\",\n success: \"bg-success-strong\",\n warning: \"bg-warning-strong\",\n danger: \"bg-destructive-strong\",\n info: \"bg-info-strong\",\n },\n },\n defaultVariants: { tone: \"neutral\" },\n },\n);\n\n/**\n * A determinate or indeterminate progress bar.\n *\n * @example\n * <Progress label=\"Upload\" value={62} />\n * <Progress label=\"Loading results\" />\n */\nexport interface ProgressProps\n extends Omit<AriaProgressBarProps, \"className\" | \"style\" | \"children\">,\n VariantProps<typeof progressFillVariants> {\n /**\n * What is progressing, and the bar's accessible name.\n *\n * **Required.** Both legacy call sites render an unnamed progressbar.\n * Pass `labelHidden` when the surrounding UI already says it.\n */\n label: ReactNode;\n /**\n * Render the label for screen readers only.\n *\n * Both legacy call sites sit beside visible text that was never\n * associated with the bar -- this is how that text becomes the name\n * without printing it twice.\n */\n labelHidden?: boolean;\n /**\n * Print the value beside the label.\n *\n * The text itself comes from React Aria, which already formats it from\n * `value`/`minValue`/`maxValue` and honours `formatOptions` and\n * `valueLabel` -- so a percentage, a currency or \"3 of 7\" are all reachable\n * without a second prop here. It was never RENDERED before: the bar\n * announced its value to assistive tech and showed a sighted reader nothing.\n */\n showValue?: boolean;\n /** Extra classes for the track. */\n trackClassName?: string;\n /** Extra classes for the fill -- a per-call-site colour the tone axis cannot express. */\n fillClassName?: string;\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nexport const Progress = forwardRef<HTMLDivElement, ProgressProps>(function Progress(\n { label, labelHidden, showValue, tone, trackClassName, fillClassName, className, ...props },\n ref,\n) {\n return (\n <AriaProgressBar\n ref={ref}\n {...props}\n data-slot=\"progress\"\n className={cn(progressVariants(), className)}\n >\n {({ percentage, isIndeterminate, valueText }) => (\n <>\n {/*\n The row exists only when the value is printed. Wrapping the label\n in a flex row unconditionally would change the label's box for\n every existing consumer to no purpose.\n */}\n <div\n data-slot=\"progress-header\"\n className={cnAll(showValue && \"flex items-baseline justify-between gap-2\")}\n >\n <AriaLabel\n data-slot=\"progress-label\"\n className={cnAll(\n \"text-sm leading-(--leading-normal) text-foreground\",\n labelHidden && \"sr-only\",\n )}\n >\n {label}\n </AriaLabel>\n {showValue ? (\n <span\n data-slot=\"progress-value\"\n // aria-hidden: React Aria already announces the value on the\n // progressbar itself. Without this a screen reader reads it\n // twice, once as the widget's value and once as loose text.\n aria-hidden=\"true\"\n className=\"text-sm leading-(--leading-normal) text-muted-foreground tabular-nums\"\n >\n {valueText}\n </span>\n ) : null}\n </div>\n <div\n data-slot=\"progress-track\"\n className={cn(progressTrackVariants(), trackClassName)}\n >\n {/*\n width, not the legacy translateX(-(100 - value)%). That\n overflowed for value > 100 -- a positive translate slid the\n indicator off to the right, while the announced value stayed\n whatever was passed. React Aria clamps the value, so the two\n can no longer disagree.\n\n An indeterminate bar renders no fill rather than the\n legacy's silent 0%: `value={undefined}` became `(value || 0)`\n there, so a sighted user saw \"0% done\" while a screen reader\n said \"indeterminate\".\n */}\n <div\n data-slot=\"progress-fill\"\n data-indeterminate={isIndeterminate || undefined}\n className={cn(progressFillVariants({ tone }), fillClassName)}\n style={{ width: isIndeterminate ? \"0%\" : `${percentage ?? 0}%` }}\n />\n </div>\n </>\n )}\n </AriaProgressBar>\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,MAAa,mBAAmB,IAAI,CAAC,mDAAmD,CAAC;AAEzF,MAAa,wBAAwB,IAAI,CACvC,+DAIA,uDACF,CAAC;AAED,MAAa,uBAAuB,IAClC,CACE,uBACA,iGACF,GACA;CACE,UAAU,EAYR,MAAM;EACJ,SAAS;EACT,SAAS;EACT,SAAS;EACT,QAAQ;EACR,MAAM;CACR,EACF;CACA,iBAAiB,EAAE,MAAM,UAAU;AACrC,CACF;AA6CA,MAAa,WAAW,WAA0C,SAAS,SACzE,EAAE,OAAO,aAAa,WAAW,MAAM,gBAAgB,eAAe,WAAW,GAAG,SACpF,KACA;CACA,OACE,oBAACA,aAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,iBAAiB,GAAG,SAAS;EAEzC,WAAA,EAAE,YAAY,iBAAiB,gBAC/B,qBAAA,UAAA,EAAA,UAAA,CAME,qBAAC,OAAD;GACE,aAAU;GACV,WAAW,MAAM,aAAa,2CAA2C;GAF3E,UAAA,CAIE,oBAACC,OAAD;IACE,aAAU;IACV,WAAW,MACT,sDACA,eAAe,SACjB;IAEC,UAAA;GACQ,CAAA,GACV,YACC,oBAAC,QAAD;IACE,aAAU;IAIV,eAAY;IACZ,WAAU;IAET,UAAA;GACG,CAAA,IACJ,IACD;EACL,CAAA,GAAA,oBAAC,OAAD;GACE,aAAU;GACV,WAAW,GAAG,sBAAsB,GAAG,cAAc;GAcrD,UAAA,oBAAC,OAAD;IACE,aAAU;IACV,sBAAoB,mBAAmB,KAAA;IACvC,WAAW,GAAG,qBAAqB,EAAE,KAAK,CAAC,GAAG,aAAa;IAC3D,OAAO,EAAE,OAAO,kBAAkB,OAAO,GAAG,cAAc,EAAE,GAAG;GAChE,CAAA;EACE,CAAA,CACL,EAAA,CAAA;CAEW,CAAA;AAErB,CAAC"}
@@ -1,5 +1,5 @@
1
- import { RadioGroupProps, RadioProps } from "react-aria-components";
2
1
  import { ReactNode } from "react";
2
+ import { RadioGroupProps, RadioProps } from "react-aria-components";
3
3
  //#region src/radio-group/radio-group.d.ts
4
4
  /**
5
5
  * RadioGroup, ported from the legacy shadcn/Radix component onto React Aria.
@@ -34,7 +34,7 @@ interface RadioGroupProps$1 extends Omit<RadioGroupProps, "className" | "style">
34
34
  /** Extra classes. Merged last, so they win conflicts with ours. */
35
35
  className?: string;
36
36
  }
37
- declare function RadioGroup$1({ className, ...props }: RadioGroupProps$1): import("react").JSX.Element;
37
+ declare const RadioGroup$1: import("react").ForwardRefExoticComponent<RadioGroupProps$1 & import("react").RefAttributes<HTMLDivElement>>;
38
38
  /**
39
39
  * One choice.
40
40
  *
@@ -47,7 +47,7 @@ interface RadioProps$1 extends Omit<RadioProps, "className" | "style" | "childre
47
47
  /** Extra classes for the label. Merged last, so they win conflicts. */
48
48
  className?: string;
49
49
  }
50
- declare function Radio$1({ className, children, ...props }: RadioProps$1): import("react").JSX.Element;
50
+ declare const Radio$1: import("react").ForwardRefExoticComponent<RadioProps$1 & import("react").RefAttributes<HTMLInputElement>>;
51
51
  //#endregion
52
52
  export { Radio$1 as Radio, RadioGroup$1 as RadioGroup, RadioGroupProps$1 as RadioGroupProps, RadioProps$1 as RadioProps };
53
53
  //# sourceMappingURL=radio-group.d.ts.map
@@ -2,6 +2,7 @@
2
2
  import { cn, cnAll } from "../lib/cn.js";
3
3
  import { selectionLabelVariants, selectionRingVariants } from "../lib/selection-control.js";
4
4
  import { cva } from "class-variance-authority";
5
+ import { forwardRef } from "react";
5
6
  import { Radio, RadioGroup } from "react-aria-components";
6
7
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
8
  //#region src/radio-group/radio-group.tsx
@@ -33,16 +34,18 @@ const radioVariants = cva([
33
34
  "group-data-[selected]:text-primary-foreground",
34
35
  selectionRingVariants()
35
36
  ]);
36
- function RadioGroup$1({ className, ...props }) {
37
+ const RadioGroup$1 = forwardRef(function RadioGroup$2({ className, ...props }, ref) {
37
38
  return /* @__PURE__ */ jsx(RadioGroup, {
39
+ ref,
38
40
  ...props,
39
41
  "data-slot": "radio-group",
40
42
  className: cn(radioGroupVariants(), className)
41
43
  });
42
- }
43
- function Radio$1({ className, children, ...props }) {
44
+ });
45
+ const Radio$1 = forwardRef(function Radio$2({ className, children, ...props }, ref) {
44
46
  return /* @__PURE__ */ jsx(Radio, {
45
47
  ...props,
48
+ inputRef: ref,
46
49
  "data-slot": "radio",
47
50
  className: cn(cnAll(selectionLabelVariants(), "justify-start"), className),
48
51
  children: ({ isSelected }) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
@@ -58,7 +61,7 @@ function Radio$1({ className, children, ...props }) {
58
61
  children
59
62
  })] })
60
63
  });
61
- }
64
+ });
62
65
  //#endregion
63
66
  export { Radio$1 as Radio, RadioGroup$1 as RadioGroup, radioGroupVariants, radioVariants };
64
67