@seekalfred/primitives 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/README.md +664 -9
  2. package/dist/accordion/accordion.d.ts +5 -5
  3. package/dist/accordion/accordion.js +22 -11
  4. package/dist/accordion/accordion.js.map +1 -1
  5. package/dist/action-button/action-button.d.ts +50 -0
  6. package/dist/action-button/action-button.js +159 -0
  7. package/dist/action-button/action-button.js.map +1 -0
  8. package/dist/action-button/index.d.ts +2 -0
  9. package/dist/action-button/index.js +2 -0
  10. package/dist/alert/alert.d.ts +16 -2
  11. package/dist/alert/alert.js +118 -20
  12. package/dist/alert/alert.js.map +1 -1
  13. package/dist/alert-dialog/alert-dialog.d.ts +1 -1
  14. package/dist/alert-dialog/alert-dialog.js +4 -2
  15. package/dist/alert-dialog/alert-dialog.js.map +1 -1
  16. package/dist/avatar/avatar.d.ts +7 -1
  17. package/dist/avatar/avatar.js +51 -18
  18. package/dist/avatar/avatar.js.map +1 -1
  19. package/dist/badge/badge.d.ts +10 -3
  20. package/dist/badge/badge.js +37 -5
  21. package/dist/badge/badge.js.map +1 -1
  22. package/dist/breadcrumb/breadcrumb.d.ts +3 -30
  23. package/dist/breadcrumb/breadcrumb.js +25 -8
  24. package/dist/breadcrumb/breadcrumb.js.map +1 -1
  25. package/dist/button/button.d.ts +36 -23
  26. package/dist/button/button.js +178 -33
  27. package/dist/button/button.js.map +1 -1
  28. package/dist/button-group/button-group.d.ts +76 -0
  29. package/dist/button-group/button-group.js +403 -0
  30. package/dist/button-group/button-group.js.map +1 -0
  31. package/dist/button-group/index.d.ts +2 -0
  32. package/dist/button-group/index.js +2 -0
  33. package/dist/calendar/calendar.d.ts +3 -3
  34. package/dist/calendar/calendar.js +263 -34
  35. package/dist/calendar/calendar.js.map +1 -1
  36. package/dist/card/card.d.ts +14 -7
  37. package/dist/card/card.js +32 -14
  38. package/dist/card/card.js.map +1 -1
  39. package/dist/checkbox/checkbox.d.ts +2 -9
  40. package/dist/checkbox/checkbox.js +28 -29
  41. package/dist/checkbox/checkbox.js.map +1 -1
  42. package/dist/confirm-morph/confirm-morph.d.ts +85 -0
  43. package/dist/confirm-morph/confirm-morph.js +652 -0
  44. package/dist/confirm-morph/confirm-morph.js.map +1 -0
  45. package/dist/confirm-morph/index.d.ts +2 -0
  46. package/dist/confirm-morph/index.js +2 -0
  47. package/dist/copy-button/copy-button.d.ts +32 -0
  48. package/dist/copy-button/copy-button.js +134 -0
  49. package/dist/copy-button/copy-button.js.map +1 -0
  50. package/dist/copy-button/index.d.ts +2 -0
  51. package/dist/copy-button/index.js +2 -0
  52. package/dist/date-picker/date-picker.d.ts +51 -0
  53. package/dist/date-picker/date-picker.js +241 -0
  54. package/dist/date-picker/date-picker.js.map +1 -0
  55. package/dist/date-picker/date-range-picker.d.ts +78 -0
  56. package/dist/date-picker/date-range-picker.js +1015 -0
  57. package/dist/date-picker/date-range-picker.js.map +1 -0
  58. package/dist/date-picker/index.d.ts +4 -0
  59. package/dist/date-picker/index.js +4 -0
  60. package/dist/date-picker/picker-parts.d.ts +46 -0
  61. package/dist/date-picker/picker-parts.js +135 -0
  62. package/dist/date-picker/picker-parts.js.map +1 -0
  63. package/dist/dialog/dialog.d.ts +12 -16
  64. package/dist/dialog/dialog.js +45 -14
  65. package/dist/dialog/dialog.js.map +1 -1
  66. package/dist/empty-state/empty-state.d.ts +44 -0
  67. package/dist/empty-state/empty-state.js +83 -0
  68. package/dist/empty-state/empty-state.js.map +1 -0
  69. package/dist/empty-state/index.d.ts +2 -0
  70. package/dist/empty-state/index.js +2 -0
  71. package/dist/expanding-button-group/expanding-button-group.d.ts +54 -0
  72. package/dist/expanding-button-group/expanding-button-group.js +350 -0
  73. package/dist/expanding-button-group/expanding-button-group.js.map +1 -0
  74. package/dist/expanding-button-group/index.d.ts +2 -0
  75. package/dist/expanding-button-group/index.js +2 -0
  76. package/dist/file-drop-zone/file-drop-zone.d.ts +71 -0
  77. package/dist/file-drop-zone/file-drop-zone.js +116 -0
  78. package/dist/file-drop-zone/file-drop-zone.js.map +1 -0
  79. package/dist/file-drop-zone/index.d.ts +2 -0
  80. package/dist/file-drop-zone/index.js +2 -0
  81. package/dist/filter-bar/filter-bar.d.ts +121 -0
  82. package/dist/filter-bar/filter-bar.js +300 -0
  83. package/dist/filter-bar/filter-bar.js.map +1 -0
  84. package/dist/filter-bar/index.d.ts +2 -0
  85. package/dist/filter-bar/index.js +2 -0
  86. package/dist/floating-button-group/floating-button-group.d.ts +61 -0
  87. package/dist/floating-button-group/floating-button-group.js +340 -0
  88. package/dist/floating-button-group/floating-button-group.js.map +1 -0
  89. package/dist/floating-button-group/index.d.ts +2 -0
  90. package/dist/floating-button-group/index.js +2 -0
  91. package/dist/icon-morph/icon-morph.d.ts +55 -0
  92. package/dist/icon-morph/icon-morph.js +331 -0
  93. package/dist/icon-morph/icon-morph.js.map +1 -0
  94. package/dist/icon-morph/index.d.ts +2 -0
  95. package/dist/icon-morph/index.js +2 -0
  96. package/dist/index.d.ts +24 -2
  97. package/dist/index.js +29 -7
  98. package/dist/input/input.d.ts +23 -1
  99. package/dist/input/input.js +37 -13
  100. package/dist/input/input.js.map +1 -1
  101. package/dist/label/label.d.ts +1 -1
  102. package/dist/label/label.js +4 -2
  103. package/dist/label/label.js.map +1 -1
  104. package/dist/lib/count-to.js +113 -0
  105. package/dist/lib/count-to.js.map +1 -0
  106. package/dist/lib/eye-morph.js +62 -0
  107. package/dist/lib/eye-morph.js.map +1 -0
  108. package/dist/lib/field-message.js +336 -0
  109. package/dist/lib/field-message.js.map +1 -0
  110. package/dist/lib/field-shell.js +81 -0
  111. package/dist/lib/field-shell.js.map +1 -0
  112. package/dist/lib/field-surface.js +4 -2
  113. package/dist/lib/field-surface.js.map +1 -1
  114. package/dist/lib/flip.js +80 -0
  115. package/dist/lib/flip.js.map +1 -0
  116. package/dist/lib/height-follow.js +59 -0
  117. package/dist/lib/height-follow.js.map +1 -0
  118. package/dist/lib/icon-swap.js +47 -0
  119. package/dist/lib/icon-swap.js.map +1 -0
  120. package/dist/lib/leaving-list.js +57 -0
  121. package/dist/lib/leaving-list.js.map +1 -0
  122. package/dist/lib/merge-refs.js +22 -0
  123. package/dist/lib/merge-refs.js.map +1 -0
  124. package/dist/lib/morph-text.js +128 -0
  125. package/dist/lib/morph-text.js.map +1 -0
  126. package/dist/lib/motion.js +162 -0
  127. package/dist/lib/motion.js.map +1 -0
  128. package/dist/lib/pausable-timeout.js +67 -0
  129. package/dist/lib/pausable-timeout.js.map +1 -0
  130. package/dist/lib/rolling-value.js +31 -0
  131. package/dist/lib/rolling-value.js.map +1 -0
  132. package/dist/lib/spring-progress.js +114 -0
  133. package/dist/lib/spring-progress.js.map +1 -0
  134. package/dist/lib/svg-id.js +10 -0
  135. package/dist/lib/svg-id.js.map +1 -0
  136. package/dist/lib/swap-text.js +77 -0
  137. package/dist/lib/swap-text.js.map +1 -0
  138. package/dist/lib/use-copy-feedback.js +36 -0
  139. package/dist/lib/use-copy-feedback.js.map +1 -0
  140. package/dist/menu/menu.d.ts +5 -5
  141. package/dist/menu/menu.js +10 -6
  142. package/dist/menu/menu.js.map +1 -1
  143. package/dist/page-header/index.d.ts +2 -0
  144. package/dist/page-header/index.js +2 -0
  145. package/dist/page-header/page-header.d.ts +75 -0
  146. package/dist/page-header/page-header.js +83 -0
  147. package/dist/page-header/page-header.js.map +1 -0
  148. package/dist/password-field/index.d.ts +2 -0
  149. package/dist/password-field/index.js +2 -0
  150. package/dist/password-field/password-field.d.ts +37 -0
  151. package/dist/password-field/password-field.js +101 -0
  152. package/dist/password-field/password-field.js.map +1 -0
  153. package/dist/password-strength/index.d.ts +2 -0
  154. package/dist/password-strength/index.js +2 -0
  155. package/dist/password-strength/password-strength.d.ts +127 -0
  156. package/dist/password-strength/password-strength.js +455 -0
  157. package/dist/password-strength/password-strength.js.map +1 -0
  158. package/dist/popover/popover.d.ts +10 -2
  159. package/dist/popover/popover.js +6 -4
  160. package/dist/popover/popover.js.map +1 -1
  161. package/dist/progress/progress.d.ts +2 -2
  162. package/dist/progress/progress.js +50 -7
  163. package/dist/progress/progress.js.map +1 -1
  164. package/dist/radio-group/radio-group.d.ts +3 -3
  165. package/dist/radio-group/radio-group.js +14 -10
  166. package/dist/radio-group/radio-group.js.map +1 -1
  167. package/dist/scroll-area/scroll-area.d.ts +17 -6
  168. package/dist/scroll-area/scroll-area.js +30 -6
  169. package/dist/scroll-area/scroll-area.js.map +1 -1
  170. package/dist/search-field/index.d.ts +2 -0
  171. package/dist/search-field/index.js +2 -0
  172. package/dist/search-field/search-field.d.ts +26 -0
  173. package/dist/search-field/search-field.js +155 -0
  174. package/dist/search-field/search-field.js.map +1 -0
  175. package/dist/select/select.d.ts +3 -3
  176. package/dist/select/select.js +85 -24
  177. package/dist/select/select.js.map +1 -1
  178. package/dist/separator/separator.d.ts +1 -1
  179. package/dist/separator/separator.js +4 -2
  180. package/dist/separator/separator.js.map +1 -1
  181. package/dist/sheet/sheet.d.ts +1 -1
  182. package/dist/sheet/sheet.js +6 -3
  183. package/dist/sheet/sheet.js.map +1 -1
  184. package/dist/side-nav/index.d.ts +2 -0
  185. package/dist/side-nav/index.js +2 -0
  186. package/dist/side-nav/side-nav.d.ts +72 -0
  187. package/dist/side-nav/side-nav.js +142 -0
  188. package/dist/side-nav/side-nav.js.map +1 -0
  189. package/dist/skeleton/skeleton.d.ts +8 -2
  190. package/dist/skeleton/skeleton.js +9 -3
  191. package/dist/skeleton/skeleton.js.map +1 -1
  192. package/dist/slider/slider.d.ts +2 -2
  193. package/dist/slider/slider.js +64 -8
  194. package/dist/slider/slider.js.map +1 -1
  195. package/dist/spinner/spinner.d.ts +1 -1
  196. package/dist/spinner/spinner.js +4 -2
  197. package/dist/spinner/spinner.js.map +1 -1
  198. package/dist/split-button/index.d.ts +2 -0
  199. package/dist/split-button/index.js +2 -0
  200. package/dist/split-button/split-button.d.ts +46 -0
  201. package/dist/split-button/split-button.js +122 -0
  202. package/dist/split-button/split-button.js.map +1 -0
  203. package/dist/stat-card/index.d.ts +2 -0
  204. package/dist/stat-card/index.js +2 -0
  205. package/dist/stat-card/stat-card.d.ts +81 -0
  206. package/dist/stat-card/stat-card.js +152 -0
  207. package/dist/stat-card/stat-card.js.map +1 -0
  208. package/dist/styles.css +1 -1
  209. package/dist/switch/switch.d.ts +2 -2
  210. package/dist/switch/switch.js +11 -8
  211. package/dist/switch/switch.js.map +1 -1
  212. package/dist/table/index.d.ts +2 -2
  213. package/dist/table/index.js +2 -2
  214. package/dist/table/table.d.ts +38 -7
  215. package/dist/table/table.js +114 -12
  216. package/dist/table/table.js.map +1 -1
  217. package/dist/tabs/tabs.d.ts +4 -4
  218. package/dist/tabs/tabs.js +153 -12
  219. package/dist/tabs/tabs.js.map +1 -1
  220. package/dist/textarea/textarea.d.ts +24 -1
  221. package/dist/textarea/textarea.js +27 -3
  222. package/dist/textarea/textarea.js.map +1 -1
  223. package/dist/theme-switch/index.d.ts +2 -0
  224. package/dist/theme-switch/index.js +2 -0
  225. package/dist/theme-switch/theme-switch.d.ts +37 -0
  226. package/dist/theme-switch/theme-switch.js +123 -0
  227. package/dist/theme-switch/theme-switch.js.map +1 -0
  228. package/dist/time-picker/index.d.ts +2 -0
  229. package/dist/time-picker/index.js +2 -0
  230. package/dist/time-picker/time-picker.d.ts +54 -0
  231. package/dist/time-picker/time-picker.js +201 -0
  232. package/dist/time-picker/time-picker.js.map +1 -0
  233. package/dist/toast/toast.d.ts +2 -2
  234. package/dist/toast/toast.js +61 -39
  235. package/dist/toast/toast.js.map +1 -1
  236. package/dist/tooltip/tooltip.d.ts +3 -3
  237. package/dist/tooltip/tooltip.js +66 -15
  238. package/dist/tooltip/tooltip.js.map +1 -1
  239. package/package.json +83 -3
@@ -1 +1 @@
1
- {"version":3,"file":"breadcrumb.js","names":["Breadcrumbs","AriaBreadcrumbs","AriaBreadcrumb","AriaLink"],"sources":["../../src/breadcrumb/breadcrumb.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport type { ReactNode } from \"react\";\nimport {\n Breadcrumb as AriaBreadcrumb,\n Breadcrumbs as AriaBreadcrumbs,\n Link as AriaLink,\n type BreadcrumbProps as AriaBreadcrumbProps,\n type BreadcrumbsProps as AriaBreadcrumbsProps,\n} from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\n\n/**\n * Breadcrumbs, ported from the legacy shadcn/Radix component onto React\n * Aria's Breadcrumbs collection.\n *\n * SIX LEGACY PARTS COLLAPSE TO TWO, and not for tidiness -- React Aria's\n * `Breadcrumbs` is a real collection, so it knows which item is last:\n *\n * | legacy | uses | fate |\n * | ------------------- | ---- | ------------------------------------------- |\n * | Breadcrumb | 2 | -> `Breadcrumbs` (nav + ol) |\n * | BreadcrumbList | 2 | dropped -- `Breadcrumbs` IS the `<ol>` |\n * | BreadcrumbItem | 6 | -> `BreadcrumbItem` |\n * | BreadcrumbLink | 4 | merged into `BreadcrumbItem` as `href` |\n * | BreadcrumbPage | 2 | dropped -- the last crumb is auto-current |\n * | BreadcrumbSeparator | 4 | dropped -- every non-last crumb draws one |\n * | BreadcrumbEllipsis | 0 | dropped -- zero call sites |\n *\n * The collection is also why the separator CANNOT stay a component: RAC\n * builds its item list from the children, so a `<BreadcrumbSeparator/>`\n * sibling would be a non-item node inside a collection and there is nowhere\n * legal to put it.\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 -- the component would render unstyled with no\n * error anywhere.\n */\nexport const breadcrumbsVariants = cva([\n \"flex flex-wrap items-center gap-2\",\n\n // The user-agent `<ol>`: decimal markers, a 40px inline padding and a 1em\n // block margin. The reset in @seekalfred/styles is OPT-IN, so a consumer\n // may load none -- unreset, the trail renders as a numbered, indented\n // list. The Accordion shipped its panel in the browser's serif for exactly\n // this family of reason.\n \"m-0 list-none p-0\",\n\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-normal) text-muted-foreground\",\n\n // box-border: content-box would add any padding a consumer sets on top of\n // a constrained width. The Alert shipped overflowing by 46px that way.\n \"box-border break-words\",\n]);\n\nexport const breadcrumbItemVariants = cva([\n // `group` is load-bearing: the separator hides itself from the current\n // crumb via group-data-[current], and RAC puts data-current on the <li>,\n // which CSS cannot look upward to from the span.\n \"group inline-flex items-center gap-2\",\n]);\n\nexport const breadcrumbLinkVariants = cva([\n \"inline-flex items-center gap-1.5\",\n\n // The user-agent `<a>`: -webkit-link blue and a permanent underline. Left\n // alone, every crumb is blue, and the hover underline below stops being a\n // state cue at all because it is already on.\n \"font-sans text-muted-foreground no-underline\",\n \"rounded-sm cursor-pointer\",\n \"motion-safe:transition-colors motion-safe:duration-(--duration-fast) motion-safe:ease-standard\",\n\n // Hover is NOT colour-only. The legacy component moved muted-foreground to\n // foreground and nothing else; under forced-colors the user's palette\n // overrides both and the cue vanishes entirely (WCAG 1.4.1, and\n // ARCHITECTURE section 5 rule 6). The underline survives any colour mode.\n \"data-[hovered]:text-foreground data-[hovered]:underline\",\n\n // outline-solid is load-bearing, not decoration. `outline-none` sets BOTH\n // outline-style:none and --tw-outline-style:none, and the width utility\n // resolves its style from that same variable -- so a focus variant can\n // apply a width and a colour while faithfully re-applying style:none and\n // painting nothing. Button shipped exactly that: keyboard-focusable with\n // no visible ring, and every gate stayed green because axe has no\n // focus-indicator rule.\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\n // The current crumb. RAC forces isDisabled on it, so React Aria renders a\n // <span role=\"link\" aria-disabled=\"true\" aria-current=\"page\"> with no\n // tabIndex -- byte for byte the legacy BreadcrumbPage, minus the chance of\n // hand-writing it onto the wrong item.\n \"data-[current]:text-foreground data-[current]:font-normal data-[current]:cursor-default\",\n\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0\",\n \"[&_svg:not([class*='size-'])]:size-(--icon-sm)\",\n]);\n\n/**\n * A navigation trail.\n *\n * The last child is the current page: give it no `href`, and React Aria\n * marks it `aria-current=\"page\"` and takes it out of the tab order.\n *\n * @example\n * <Breadcrumbs>\n * <BreadcrumbItem href=\"/\">Home</BreadcrumbItem>\n * <BreadcrumbItem href=\"/design-thinking\">Design Thinking</BreadcrumbItem>\n * <BreadcrumbItem>User Personas</BreadcrumbItem>\n * </Breadcrumbs>\n */\nexport interface BreadcrumbsProps\n extends Omit<\n AriaBreadcrumbsProps<object>,\n \"className\" | \"style\" | \"children\" | \"items\" | \"dependencies\"\n > {\n /**\n * `BreadcrumbItem` elements. Static children only: `items` and the render\n * function are not exposed, because no call site builds a trail from data\n * and the generic they require leaks into every consumer's types.\n */\n children?: ReactNode;\n /**\n * Extra classes, merged last so they win conflicts with ours.\n *\n * They land on the `<ol>`, where our own classes are, so `tailwind-merge`\n * can actually resolve a conflict. The `<nav>` is an unstyled landmark\n * wrapper.\n */\n className?: string;\n /**\n * Accessible name for the navigation landmark. Only worth changing when a\n * page has more than one trail.\n *\n * @default \"Breadcrumb\"\n */\n \"aria-label\"?: string;\n}\n\nexport function Breadcrumbs({\n className,\n children,\n \"aria-label\": ariaLabel = \"Breadcrumb\",\n ...props\n}: BreadcrumbsProps) {\n return (\n // RAC renders the <ol> and puts its aria-label there; it does NOT render\n // a landmark. Dropping the <nav> would silently remove the thing screen\n // reader users actually navigate by, so it is added back here. Both\n // carry the same name deliberately -- RAC falls back to a localized\n // \"Breadcrumbs\" when aria-label is absent, so the <ol> is named either\n // way and letting the two names diverge would be the worse outcome.\n <nav aria-label={ariaLabel} data-slot=\"breadcrumb\">\n <AriaBreadcrumbs\n {...props}\n aria-label={ariaLabel}\n data-slot=\"breadcrumb-list\"\n className={cn(breadcrumbsVariants(), className)}\n >\n {children}\n </AriaBreadcrumbs>\n </nav>\n );\n}\n\n/**\n * One crumb.\n *\n * @example\n * <BreadcrumbItem href=\"/reports\">Reports</BreadcrumbItem>\n */\nexport interface BreadcrumbItemProps\n extends Omit<AriaBreadcrumbProps, \"className\" | \"style\" | \"children\"> {\n /**\n * The crumb's label.\n *\n * An icon-only crumb has no accessible name, which is an axe `link-name`\n * violation -- both legacy call sites ship one. Pass `aria-label` when the\n * only child is a glyph.\n */\n children?: ReactNode;\n /** Extra classes for the `<li>`, merged last. */\n className?: string;\n /** Destination. Omit it on the last crumb: that one is the current page. */\n href?: string;\n /** Anchor target, forwarded when `href` is set. */\n target?: string;\n /** Anchor rel, forwarded when `href` is set. */\n rel?: string;\n}\n\nexport function BreadcrumbItem({\n className,\n children,\n href,\n target,\n rel,\n \"aria-label\": ariaLabel,\n ...props\n}: BreadcrumbItemProps) {\n return (\n <AriaBreadcrumb\n {...props}\n data-slot=\"breadcrumb-item\"\n className={cn(breadcrumbItemVariants(), className)}\n >\n <AriaLink\n href={href}\n target={target}\n rel={rel}\n aria-label={ariaLabel}\n data-slot=\"breadcrumb-link\"\n className={breadcrumbLinkVariants()}\n >\n {children}\n </AriaLink>\n {/*\n A real element, not a ::after. Generated content is announced by some\n screen readers, and it cannot be read back from the DOM at all -- a\n separator that stopped rendering would be invisible to the suite.\n\n \"/\" rather than the legacy lucide ChevronRight: this library ships no\n icon dependency, all four legacy separators are the default, and a\n chevron glyph does not mirror in RTL while a slash needs no mirroring\n at all.\n */}\n <span\n aria-hidden=\"true\"\n data-slot=\"breadcrumb-separator\"\n className=\"shrink-0 select-none group-data-[current]:hidden\"\n >\n /\n </span>\n </AriaBreadcrumb>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,MAAa,sBAAsB,IAAI;CACrC;CAOA;CAIA;CAIA;AACF,CAAC;AAED,MAAa,yBAAyB,IAAI,CAIxC,sCACF,CAAC;AAED,MAAa,yBAAyB,IAAI;CACxC;CAKA;CACA;CACA;CAMA;CASA;CACA;CACA;CACA;CACA;CAMA;CAEA;CACA;AACF,CAAC;AA2CD,SAAgBA,cAAY,EAC1B,WACA,UACA,cAAc,YAAY,cAC1B,GAAG,SACgB;CACnB,OAOE,oBAAC,OAAD;EAAK,cAAY;EAAW,aAAU;EACpC,UAAA,oBAACC,aAAD;GACE,GAAI;GACJ,cAAY;GACZ,aAAU;GACV,WAAW,GAAG,oBAAoB,GAAG,SAAS;GAE7C;EACc,CAAA;CACd,CAAA;AAET;AA4BA,SAAgB,eAAe,EAC7B,WACA,UACA,MACA,QACA,KACA,cAAc,WACd,GAAG,SACmB;CACtB,OACE,qBAACC,YAAD;EACE,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,uBAAuB,GAAG,SAAS;EAHnD,UAAA,CAKE,oBAACC,MAAD;GACQ;GACE;GACH;GACL,cAAY;GACZ,aAAU;GACV,WAAW,uBAAuB;GAEjC;EACO,CAAA,GAWV,oBAAC,QAAD;GACE,eAAY;GACZ,aAAU;GACV,WAAU;GACX,UAAA;EAEK,CAAA,CACQ;;AAEpB"}
1
+ {"version":3,"file":"breadcrumb.js","names":["Breadcrumbs","AriaBreadcrumbs","AriaBreadcrumb","AriaLink"],"sources":["../../src/breadcrumb/breadcrumb.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport {\n Children,\n createContext,\n forwardRef,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from \"react\";\nimport {\n Breadcrumb as AriaBreadcrumb,\n Breadcrumbs as AriaBreadcrumbs,\n Link as AriaLink,\n type BreadcrumbProps as AriaBreadcrumbProps,\n type BreadcrumbsProps as AriaBreadcrumbsProps,\n} from \"react-aria-components\";\n\nimport { cn, cnAll } from \"../lib/cn.js\";\nimport { useFlip } from \"../lib/flip.js\";\nimport { mergeRefs } from \"../lib/merge-refs.js\";\n\n/**\n * Breadcrumbs, ported from the legacy shadcn/Radix component onto React\n * Aria's Breadcrumbs collection.\n *\n * SIX LEGACY PARTS COLLAPSE TO TWO, and not for tidiness -- React Aria's\n * `Breadcrumbs` is a real collection, so it knows which item is last:\n *\n * | legacy | uses | fate |\n * | ------------------- | ---- | ------------------------------------------- |\n * | Breadcrumb | 2 | -> `Breadcrumbs` (nav + ol) |\n * | BreadcrumbList | 2 | dropped -- `Breadcrumbs` IS the `<ol>` |\n * | BreadcrumbItem | 6 | -> `BreadcrumbItem` |\n * | BreadcrumbLink | 4 | merged into `BreadcrumbItem` as `href` |\n * | BreadcrumbPage | 2 | dropped -- the last crumb is auto-current |\n * | BreadcrumbSeparator | 4 | dropped -- every non-last crumb draws one |\n * | BreadcrumbEllipsis | 0 | dropped -- zero call sites |\n *\n * The collection is also why the separator CANNOT stay a component: RAC\n * builds its item list from the children, so a `<BreadcrumbSeparator/>`\n * sibling would be a non-item node inside a collection and there is nowhere\n * legal to put it.\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 -- the component would render unstyled with no\n * error anywhere.\n */\n/** True once the first render is done: a crumb that mounts after that slides in; the ones that were there at first are still. */\nconst TrailReady = createContext(false);\n\nexport const breadcrumbsVariants = cva([\n \"flex flex-wrap items-center gap-2\",\n\n // The user-agent `<ol>`: decimal markers, a 40px inline padding and a 1em\n // block margin. The reset in @seekalfred/styles is OPT-IN, so a consumer\n // may load none -- unreset, the trail renders as a numbered, indented\n // list. The Accordion shipped its panel in the browser's serif for exactly\n // this family of reason.\n \"m-0 list-none p-0\",\n\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-normal) text-muted-foreground\",\n\n // box-border: content-box would add any padding a consumer sets on top of\n // a constrained width. The Alert shipped overflowing by 46px that way.\n \"box-border break-words\",\n]);\n\nexport const breadcrumbItemVariants = cva([\n // `group` is load-bearing: the separator hides itself from the current\n // crumb via group-data-[current], and RAC puts data-current on the <li>,\n // which CSS cannot look upward to from the span.\n \"group inline-flex items-center gap-2\",\n]);\n\nexport const breadcrumbLinkVariants = cva([\n \"inline-flex items-center gap-1.5\",\n\n // The user-agent `<a>`: -webkit-link blue and a permanent underline. Left\n // alone, every crumb is blue, and the hover underline below stops being a\n // state cue at all because it is already on.\n \"font-sans text-muted-foreground no-underline\",\n \"rounded-sm cursor-pointer\",\n \"motion-safe:transition-colors motion-safe:duration-(--duration-quick) motion-safe:ease-settle\",\n\n // Hover is NOT colour-only. The legacy component moved muted-foreground to\n // foreground and nothing else; under forced-colors the user's palette\n // overrides both and the cue vanishes entirely (WCAG 1.4.1, and\n // ARCHITECTURE section 5 rule 6). The underline survives any colour mode.\n \"data-[hovered]:text-foreground data-[hovered]:underline\",\n\n // outline-solid is load-bearing, not decoration. `outline-none` sets BOTH\n // outline-style:none and --tw-outline-style:none, and the width utility\n // resolves its style from that same variable -- so a focus variant can\n // apply a width and a colour while faithfully re-applying style:none and\n // painting nothing. Button shipped exactly that: keyboard-focusable with\n // no visible ring, and every gate stayed green because axe has no\n // focus-indicator rule.\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\n // The current crumb. RAC forces isDisabled on it, so React Aria renders a\n // <span role=\"link\" aria-disabled=\"true\" aria-current=\"page\"> with no\n // tabIndex -- byte for byte the legacy BreadcrumbPage, minus the chance of\n // hand-writing it onto the wrong item.\n \"data-[current]:text-foreground data-[current]:font-normal data-[current]:cursor-default\",\n\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0\",\n \"[&_svg:not([class*='size-'])]:size-(--icon-sm)\",\n]);\n\n/**\n * A navigation trail.\n *\n * The last child is the current page: give it no `href`, and React Aria\n * marks it `aria-current=\"page\"` and takes it out of the tab order.\n *\n * @example\n * <Breadcrumbs>\n * <BreadcrumbItem href=\"/\">Home</BreadcrumbItem>\n * <BreadcrumbItem href=\"/design-thinking\">Design Thinking</BreadcrumbItem>\n * <BreadcrumbItem>User Personas</BreadcrumbItem>\n * </Breadcrumbs>\n */\nexport interface BreadcrumbsProps\n extends Omit<\n AriaBreadcrumbsProps<object>,\n \"className\" | \"style\" | \"children\" | \"items\" | \"dependencies\"\n > {\n /**\n * `BreadcrumbItem` elements. Static children only: `items` and the render\n * function are not exposed, because no call site builds a trail from data\n * and the generic they require leaks into every consumer's types.\n */\n children?: ReactNode;\n /**\n * Extra classes, merged last so they win conflicts with ours.\n *\n * They land on the `<ol>`, where our own classes are, so `tailwind-merge`\n * can actually resolve a conflict. The `<nav>` is an unstyled landmark\n * wrapper.\n */\n className?: string;\n /**\n * Accessible name for the navigation landmark. Only worth changing when a\n * page has more than one trail.\n *\n * @default \"Breadcrumb\"\n */\n \"aria-label\"?: string;\n}\n\nexport const Breadcrumbs = forwardRef<HTMLOListElement, BreadcrumbsProps>(function Breadcrumbs(\n { className, children, \"aria-label\": ariaLabel = \"Breadcrumb\", ...props },\n ref,\n) {\n const list = useRef<HTMLOListElement | null>(null);\n const refs = useMemo(() => mergeRefs(ref, list), [ref]);\n const [ready, setReady] = useState(false);\n useEffect(() => setReady(true), []);\n // When the trail changes, the crumbs that stay slide to their new place.\n useFlip(list, Children.toArray(children).map((child) => (child as { key?: string }).key).join(\"/\"));\n return (\n // RAC renders the <ol> and puts its aria-label there; it does NOT render\n // a landmark. Dropping the <nav> would silently remove the thing screen\n // reader users actually navigate by, so it is added back here. Both\n // carry the same name deliberately -- RAC falls back to a localized\n // \"Breadcrumbs\" when aria-label is absent, so the <ol> is named either\n // way and letting the two names diverge would be the worse outcome.\n <nav aria-label={ariaLabel} data-slot=\"breadcrumb\">\n <AriaBreadcrumbs\n // The ref goes on the <ol>, not the <nav>: the nav is this\n // component's OWN added landmark wrapper, not something React Aria\n // renders, so the real list element is one level in.\n ref={refs}\n {...props}\n aria-label={ariaLabel}\n data-slot=\"breadcrumb-list\"\n className={cn(breadcrumbsVariants(), className)}\n >\n <TrailReady.Provider value={ready}>{children}</TrailReady.Provider>\n </AriaBreadcrumbs>\n </nav>\n );\n});\n\n/**\n * One crumb.\n *\n * @example\n * <BreadcrumbItem href=\"/reports\">Reports</BreadcrumbItem>\n */\nexport interface BreadcrumbItemProps\n extends Omit<AriaBreadcrumbProps, \"className\" | \"style\" | \"children\"> {\n /**\n * The crumb's label.\n *\n * An icon-only crumb has no accessible name, which is an axe `link-name`\n * violation -- both legacy call sites ship one. Pass `aria-label` when the\n * only child is a glyph.\n */\n children?: ReactNode;\n /** Extra classes for the `<li>`, merged last. */\n className?: string;\n /** Destination. Omit it on the last crumb: that one is the current page. */\n href?: string;\n /** Anchor target, forwarded when `href` is set. */\n target?: string;\n /** Anchor rel, forwarded when `href` is set. */\n rel?: string;\n}\n\nexport const BreadcrumbItem = forwardRef<HTMLAnchorElement, BreadcrumbItemProps>(\n function BreadcrumbItem(\n { className, children, href, target, rel, \"aria-label\": ariaLabel, ...props },\n ref,\n ) {\n // Decided once, at this crumb's own mount.\n const [arrivedLater] = useState(useContext(TrailReady));\n return (\n <AriaBreadcrumb\n {...props}\n data-slot=\"breadcrumb-item\"\n data-flip=\"\"\n className={cnAll(\n breadcrumbItemVariants(),\n arrivedLater && \"motion-safe:animate-crumb-in\",\n className,\n )}\n >\n <AriaLink\n // The ref goes on the anchor, not the <li> RAC wraps it in: the list\n // item is the collection's own item container, not the interactive\n // element -- the link is what a call site actually needs to focus.\n ref={ref}\n href={href}\n target={target}\n rel={rel}\n aria-label={ariaLabel}\n data-slot=\"breadcrumb-link\"\n className={breadcrumbLinkVariants()}\n >\n {children}\n </AriaLink>\n {/*\n A real element, not a ::after. Generated content is announced by some\n screen readers, and it cannot be read back from the DOM at all -- a\n separator that stopped rendering would be invisible to the suite.\n\n \"/\" rather than the legacy lucide ChevronRight: this library ships no\n icon dependency, all four legacy separators are the default, and a\n chevron glyph does not mirror in RTL while a slash needs no mirroring\n at all.\n */}\n <span\n aria-hidden=\"true\"\n data-slot=\"breadcrumb-separator\"\n className=\"shrink-0 select-none group-data-[current]:hidden\"\n >\n /\n </span>\n </AriaBreadcrumb>\n );\n },\n);\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsDA,MAAM,aAAa,cAAc,KAAK;AAEtC,MAAa,sBAAsB,IAAI;CACrC;CAOA;CAIA;CAIA;AACF,CAAC;AAED,MAAa,yBAAyB,IAAI,CAIxC,sCACF,CAAC;AAED,MAAa,yBAAyB,IAAI;CACxC;CAKA;CACA;CACA;CAMA;CASA;CACA;CACA;CACA;CACA;CAMA;CAEA;CACA;AACF,CAAC;AA2CD,MAAaA,gBAAc,WAA+C,SAASA,cACjF,EAAE,WAAW,UAAU,cAAc,YAAY,cAAc,GAAG,SAClE,KACA;CACA,MAAM,OAAO,OAAgC,IAAI;CACjD,MAAM,OAAO,cAAc,UAAU,KAAK,IAAI,GAAG,CAAC,GAAG,CAAC;CACtD,MAAM,CAAC,OAAO,YAAY,SAAS,KAAK;CACxC,gBAAgB,SAAS,IAAI,GAAG,CAAC,CAAC;CAElC,QAAQ,MAAM,SAAS,QAAQ,QAAQ,CAAC,CAAC,KAAK,UAAW,MAA2B,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC;CAClG,OAOE,oBAAC,OAAD;EAAK,cAAY;EAAW,aAAU;EACpC,UAAA,oBAACC,aAAD;GAIE,KAAK;GACL,GAAI;GACJ,cAAY;GACZ,aAAU;GACV,WAAW,GAAG,oBAAoB,GAAG,SAAS;GAE9C,UAAA,oBAAC,WAAW,UAAZ;IAAqB,OAAO;IAAQ;GAA8B,CAAA;EACnD,CAAA;CACd,CAAA;AAET,CAAC;AA4BD,MAAa,iBAAiB,WAC5B,SAAS,eACP,EAAE,WAAW,UAAU,MAAM,QAAQ,KAAK,cAAc,WAAW,GAAG,SACtE,KACA;CAEA,MAAM,CAAC,gBAAgB,SAAS,WAAW,UAAU,CAAC;CACtD,OACE,qBAACC,YAAD;EACE,GAAI;EACJ,aAAU;EACV,aAAU;EACV,WAAW,MACT,uBAAuB,GACvB,gBAAgB,gCAChB,SACF;EARF,UAAA,CAUE,oBAACC,MAAD;GAIO;GACC;GACE;GACH;GACL,cAAY;GACZ,aAAU;GACV,WAAW,uBAAuB;GAEjC;EACO,CAAA,GAWV,oBAAC,QAAD;GACE,eAAY;GACZ,aAAU;GACV,WAAU;GACX,UAAA;EAEK,CAAA,CACQ;;AAEpB,CACF"}
@@ -1,27 +1,9 @@
1
1
  import { VariantProps } from "class-variance-authority";
2
- import { ButtonProps } from "react-aria-components";
3
2
  import { ReactNode } from "react";
3
+ import { ButtonProps, LinkProps } from "react-aria-components";
4
4
  //#region src/button/button.d.ts
5
- /**
6
- * Variant classes, exported so a consumer can reuse the exact classes we ship
7
- * rather than guessing at them. This is also the supported answer for the
8
- * link-shaped cases React Aria's Button cannot cover: it never renders an
9
- * anchor, so `<a className={buttonVariants({ variant: "outline" })}>` is how a
10
- * real navigation target gets button styling until the Link primitive lands.
11
- *
12
- * Ported from the legacy shadcn Button. The variant and size vocabulary is
13
- * deliberately identical so existing call sites migrate by rename, with six
14
- * corrections that the Alfred contracts require -- each marked below.
15
- *
16
- * Every string here is a STATIC literal. Tailwind scans source text, so a class
17
- * assembled at runtime is invisible to it and would simply never be emitted.
18
- *
19
- * State comes from React Aria data attributes rather than pseudo-classes:
20
- * data-[focus-visible] is keyboard focus only, and data-[pressed] covers mouse,
21
- * touch and keyboard uniformly. The original had no pressed state at all.
22
- */
23
5
  export declare const buttonVariants: (props?: ({
24
- variant?: "primary" | "secondary" | "outline" | "outlineNeutral" | "ghost" | "destructive" | "link" | null | undefined;
6
+ variant?: "link" | "destructive" | "primary" | "secondary" | "outline" | "outlineNeutral" | "ghost" | null | undefined;
25
7
  size?: "sm" | "md" | "lg" | "icon" | null | undefined;
26
8
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
27
9
  /**
@@ -30,8 +12,8 @@ export declare const buttonVariants: (props?: ({
30
12
  * CORRECTION 6 -- the legacy Button was a plain `<button>`, so it took
31
13
  * `onClick`, `disabled` and Radix's `asChild`. React Aria's Button has none of
32
14
  * those: `ButtonProps` explicitly omits `onClick` and `href`. The replacements
33
- * are `onPress`, `isDisabled`, and `buttonVariants()` on the consumer's own
34
- * element. See the migration table in the package README.
15
+ * are `onPress`, `isDisabled`, and `href` -- which renders React Aria's Link in
16
+ * place of its Button. See the migration table in the package README.
35
17
  *
36
18
  * Deliberately NOT intersected with ComponentPropsWithoutRef<"button">: that
37
19
  * would advertise disabled, tabIndex and role, which React Aria does not
@@ -59,10 +41,40 @@ interface ButtonProps$1 extends Omit<ButtonProps, "className" | "style">, Varian
59
41
  startIcon?: ReactNode;
60
42
  /** Icon rendered after the label. Same rules as {@link ButtonProps.startIcon}. */
61
43
  endIcon?: ReactNode;
44
+ /**
45
+ * Makes the button a link. It renders React Aria's `Link` -- a real `<a>`,
46
+ * announced as a link -- with exactly the classes a button of the same
47
+ * `variant` and `size` gets, and the same hover, press and focus-ring states.
48
+ *
49
+ * Use it whenever activating the control navigates. A button that navigates
50
+ * cannot be opened in a new tab, is announced as the wrong role, and is
51
+ * skipped by a screen reader's list of links.
52
+ *
53
+ * Client-side routing needs React Aria's `RouterProvider` mounted once above
54
+ * it; without one, an `href` is an ordinary page load, which is also what an
55
+ * external or download link wants.
56
+ *
57
+ * Button-only props have nothing to act on here: `type`, `form*` and
58
+ * `isPending` are ignored, and the `ref` receives the `<a>`.
59
+ */
60
+ href?: LinkProps["href"];
61
+ /** With `href` only: where to open it, e.g. `"_blank"`. */
62
+ target?: LinkProps["target"];
63
+ /** With `href` only. Give `"noreferrer"` alongside `target="_blank"` to an external site. */
64
+ rel?: LinkProps["rel"];
65
+ /** With `href` only: download the target instead of opening it, optionally under this file name. */
66
+ download?: LinkProps["download"];
62
67
  }
63
68
  /**
64
69
  * A button.
65
70
  *
71
+ * A label that changes (a different text, or a different icon) crossfades, and
72
+ * the button's width springs to fit it. While that runs, the OUTGOING label is
73
+ * still in the DOM for about 200ms, `aria-hidden` and out of flow, so read a
74
+ * button's text after the change settles, and do not put `id`s, inputs or
75
+ * anything stateful inside a label that changes. Text that changes constantly,
76
+ * such as a live counter, crossfades every time it does.
77
+ *
66
78
  * `size="icon"` renders a square button with no visible text, so it **must**
67
79
  * carry an `aria-label` -- otherwise it has no accessible name and fails axe's
68
80
  * `button-name` rule. The IconOnly story is the guard on that.
@@ -72,8 +84,9 @@ interface ButtonProps$1 extends Omit<ButtonProps, "className" | "style">, Varian
72
84
  * <Button startIcon={<SaveIcon aria-hidden />}>Save changes</Button>
73
85
  * <Button endIcon={<ChevronIcon aria-hidden />}>Continue</Button>
74
86
  * <Button variant="outline" size="icon" aria-label="Close"><XIcon /></Button>
87
+ * <Button href="/reports" variant="outlineNeutral">View reports</Button>
75
88
  */
76
- declare function Button$1({ className, variant, size, startIcon, endIcon, children, ...props }: ButtonProps$1): import("react").JSX.Element;
89
+ declare const Button$1: import("react").ForwardRefExoticComponent<ButtonProps$1 & import("react").RefAttributes<HTMLButtonElement>>;
77
90
  //#endregion
78
91
  export { Button$1 as Button, ButtonProps$1 as ButtonProps };
79
92
  //# sourceMappingURL=button.d.ts.map
@@ -1,15 +1,20 @@
1
1
  "use client";
2
2
  import { cn } from "../lib/cn.js";
3
+ import { mergeRefs } from "../lib/merge-refs.js";
4
+ import { anchorsLayer, pressScaleFor, useAriaBusy, useExitBackstop, useLeavingItems, useMorphWidth } from "../lib/motion.js";
3
5
  import { cva } from "class-variance-authority";
4
- import { Button } from "react-aria-components";
5
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
+ import { forwardRef, isValidElement, useCallback, useRef, useState } from "react";
7
+ import { Button, Link } from "react-aria-components";
8
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
6
9
  //#region src/button/button.tsx
7
10
  /**
8
11
  * Variant classes, exported so a consumer can reuse the exact classes we ship
9
- * rather than guessing at them. This is also the supported answer for the
10
- * link-shaped cases React Aria's Button cannot cover: it never renders an
11
- * anchor, so `<a className={buttonVariants({ variant: "outline" })}>` is how a
12
- * real navigation target gets button styling until the Link primitive lands.
12
+ * rather than guessing at them.
13
+ *
14
+ * NOT the answer for a link-shaped button any more: pass `href` to Button. On a
15
+ * consumer's own <a> these classes get none of their states -- hover, press and
16
+ * the focus ring all key on React Aria data attributes that a plain anchor never
17
+ * receives, and the base sets outline-none, so keyboard focus was invisible.
13
18
  *
14
19
  * Ported from the legacy shadcn Button. The variant and size vocabulary is
15
20
  * deliberately identical so existing call sites migrate by rename, with six
@@ -22,6 +27,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
22
27
  * data-[focus-visible] is keyboard focus only, and data-[pressed] covers mouse,
23
28
  * touch and keyboard uniformly. The original had no pressed state at all.
24
29
  */
30
+ const pressScale = "motion-safe:data-[pressed]:scale-(--press-scale) [&:is([aria-haspopup]:not([aria-haspopup=false]),[data-state],[role=combobox])]:scale-none!";
25
31
  const buttonVariants = cva([
26
32
  "inline-flex items-center justify-center gap-(--density-control-gap) shrink-0 whitespace-nowrap",
27
33
  "rounded-md font-sans font-medium",
@@ -34,49 +40,64 @@ const buttonVariants = cva([
34
40
  "data-[focus-visible]:outline-(length:--focus-ring-width)",
35
41
  "data-[focus-visible]:outline-offset-(--focus-ring-offset)",
36
42
  "data-[focus-visible]:outline-(--color-ring)",
37
- "data-[disabled]:cursor-not-allowed data-[disabled]:opacity-(--opacity-disabled)",
43
+ "data-[disabled]:cursor-not-allowed",
44
+ "[-webkit-tap-highlight-color:transparent]",
38
45
  "data-[pending]:cursor-progress"
39
46
  ], {
40
47
  variants: {
41
48
  variant: {
42
49
  primary: [
43
50
  "bg-primary text-primary-foreground shadow-surface",
44
- "motion-safe:transition-colors motion-safe:duration-(--duration-fast) motion-safe:ease-standard",
45
- "data-[hovered]:bg-primary-hover",
46
- "data-[pressed]:bg-primary-pressed"
51
+ "motion-safe:button-motion",
52
+ pressScale,
53
+ "data-[hovered]:opacity-91 data-[hovered]:shadow-resting",
54
+ "data-[pressed]:opacity-84",
55
+ "data-[disabled]:opacity-(--opacity-button-disabled)"
47
56
  ],
48
57
  secondary: [
49
58
  "bg-secondary text-secondary-foreground shadow-surface",
50
- "motion-safe:transition-colors",
51
- "data-[hovered]:bg-secondary-hover",
52
- "data-[pressed]:bg-secondary-pressed"
59
+ "motion-safe:button-motion",
60
+ pressScale,
61
+ "data-[hovered]:bg-secondary-hover data-[hovered]:shadow-resting",
62
+ "data-[pressed]:bg-secondary-pressed",
63
+ "data-[disabled]:opacity-100 data-[disabled]:bg-muted data-[disabled]:text-muted-foreground data-[disabled]:border-border"
53
64
  ],
54
65
  outline: [
55
66
  "bg-background text-primary-strong border-(length:--border-width) border-primary-strong shadow-surface",
56
- "motion-safe:transition-colors",
57
- "data-[hovered]:bg-muted",
58
- "data-[pressed]:bg-muted data-[pressed]:border-foreground"
67
+ "motion-safe:button-motion",
68
+ pressScale,
69
+ "data-[hovered]:bg-muted data-[hovered]:shadow-resting",
70
+ "data-[pressed]:bg-muted data-[pressed]:border-foreground",
71
+ "data-[disabled]:opacity-100 data-[disabled]:bg-muted data-[disabled]:text-muted-foreground data-[disabled]:border-border"
59
72
  ],
60
73
  outlineNeutral: [
61
74
  "bg-background text-foreground border-(length:--border-width) border-border shadow-surface",
62
- "motion-safe:transition-colors",
63
- "data-[hovered]:bg-muted",
64
- "data-[pressed]:bg-muted data-[pressed]:border-foreground"
75
+ "motion-safe:button-motion",
76
+ pressScale,
77
+ "data-[hovered]:bg-muted data-[hovered]:shadow-resting",
78
+ "data-[pressed]:bg-muted data-[pressed]:border-foreground",
79
+ "data-[disabled]:opacity-100 data-[disabled]:bg-muted data-[disabled]:text-muted-foreground"
65
80
  ],
66
81
  ghost: [
67
82
  "bg-transparent text-primary-strong",
68
- "motion-safe:transition-colors",
83
+ "motion-safe:button-motion",
84
+ pressScale,
69
85
  "data-[hovered]:bg-muted",
86
+ "data-[disabled]:opacity-100 data-[disabled]:text-muted-foreground",
70
87
  "data-[pressed]:bg-muted data-[pressed]:border-primary-strong"
71
88
  ],
72
89
  destructive: [
73
90
  "bg-destructive text-destructive-foreground shadow-surface",
74
- "motion-safe:transition-colors",
91
+ "motion-safe:button-motion",
92
+ pressScale,
75
93
  "data-[hovered]:bg-destructive-hover",
76
- "data-[pressed]:bg-destructive-pressed"
94
+ "data-[pressed]:bg-destructive-pressed",
95
+ "data-[disabled]:opacity-100 data-[disabled]:bg-muted data-[disabled]:border-border data-[disabled]:text-[color-mix(in_oklab,var(--color-destructive)_55%,var(--color-muted))]"
77
96
  ],
78
97
  link: [
79
98
  "bg-transparent text-primary-strong underline-offset-(--underline-offset)",
99
+ "motion-safe:button-motion",
100
+ "data-[disabled]:opacity-(--opacity-button-disabled)",
80
101
  "data-[hovered]:underline",
81
102
  "data-[pressed]:underline"
82
103
  ]
@@ -93,9 +114,101 @@ const buttonVariants = cva([
93
114
  size: "md"
94
115
  }
95
116
  });
117
+ /** A key for the label content: text plus element names, so a new label or icon crossfades while prop-only updates stay in place. */
118
+ function labelKey(node) {
119
+ if (node == null || typeof node === "boolean") return "";
120
+ if (typeof node === "string" || typeof node === "number" || typeof node === "bigint") return String(node);
121
+ if (Array.isArray(node)) return node.map(labelKey).join("");
122
+ if (!isValidElement(node)) return "";
123
+ const type = node.type;
124
+ return "<" + (typeof type === "string" ? type : type?.displayName ?? type?.name ?? "") + ">" + labelKey(node.props.children);
125
+ }
126
+ /** A label with no text once element names are stripped is an icon, which scales instead of rising. */
127
+ const isIconKey = (key) => !/\S/.test(key.replace(/<[^>]*>/g, ""));
128
+ function LeavingPhase({ phase, onDone }) {
129
+ const { id } = phase;
130
+ const remove = useCallback(() => onDone(id), [onDone, id]);
131
+ useExitBackstop(true, remove);
132
+ return /* @__PURE__ */ jsx("span", {
133
+ "aria-hidden": "true",
134
+ onAnimationEnd: (event) => {
135
+ if (event.target === event.currentTarget) remove();
136
+ },
137
+ style: phase.rect,
138
+ className: cn("absolute inline-flex items-center justify-center gap-(--density-control-gap) whitespace-nowrap [&_svg]:shrink-0", phase.icon ? "motion-safe:animate-glyph-out motion-reduce:animate-fade-out" : "motion-safe:animate-label-out motion-reduce:animate-fade-out"),
139
+ children: phase.node
140
+ });
141
+ }
142
+ /** The loading spinner: it scales in on the snappy spring, and out smaller with a little blur. Nothing animates if it is there on first mount. */
143
+ function Loader({ show }) {
144
+ const [initial] = useState(show);
145
+ const [rendered, setRendered] = useState(show);
146
+ const [animated, setAnimated] = useState(false);
147
+ const leaving = rendered && !show;
148
+ if (show && !rendered) setRendered(true);
149
+ if (!animated && show !== initial) setAnimated(true);
150
+ const unmount = useCallback(() => setRendered(false), []);
151
+ useExitBackstop(leaving, unmount);
152
+ if (!rendered) return null;
153
+ return /* @__PURE__ */ jsx("span", {
154
+ "aria-hidden": "true",
155
+ onAnimationEnd: (event) => {
156
+ if (leaving && event.target === event.currentTarget) unmount();
157
+ },
158
+ className: cn("absolute top-1/2 left-1/2 -m-2 grid size-4", leaving ? "motion-safe:animate-loader-out motion-reduce:animate-fade-out" : animated && "motion-safe:animate-loader-in motion-reduce:animate-fade-in"),
159
+ children: /* @__PURE__ */ jsx("span", { className: "rounded-circle border-(length:--border-width-spinner) border-current border-r-transparent motion-safe:animate-spin-fast" })
160
+ });
161
+ }
162
+ /**
163
+ * The button's inside: a loader, and a slot that holds the current label.
164
+ *
165
+ * A new label (a different text, or a different icon) mounts under a new key and
166
+ * rises in while the old one is popped out of flow where it was and lifts away.
167
+ * Nothing animates on first mount, and a prop-only update (same key) stays in
168
+ * place.
169
+ */
170
+ function ButtonContent({ children, pending }) {
171
+ const key = labelKey(children);
172
+ const icon = isIconKey(key);
173
+ const contentRef = useRef(null);
174
+ const phaseRef = useRef(null);
175
+ useMorphWidth(contentRef, key);
176
+ const { leaving, changed, remove } = useLeavingItems(key, {
177
+ node: children,
178
+ icon
179
+ }, () => phaseRef.current);
180
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Loader, { show: pending }), /* @__PURE__ */ jsx("span", {
181
+ className: cn("button-slot", pending && "opacity-0"),
182
+ children: /* @__PURE__ */ jsxs("span", {
183
+ ref: contentRef,
184
+ className: "inline-flex flex-none items-center",
185
+ children: [leaving.map((item) => /* @__PURE__ */ jsx(LeavingPhase, {
186
+ phase: {
187
+ id: item.id,
188
+ node: item.value.node,
189
+ icon: item.value.icon,
190
+ rect: item.rect
191
+ },
192
+ onDone: remove
193
+ }, item.id)), /* @__PURE__ */ jsx("span", {
194
+ ref: phaseRef,
195
+ "data-slot": "button-label",
196
+ className: cn("inline-flex items-center justify-center gap-(--density-control-gap) whitespace-nowrap [&_svg]:shrink-0", changed && (icon ? "motion-safe:animate-glyph-in motion-reduce:animate-fade-in" : "motion-safe:animate-label-in motion-reduce:animate-fade-in")),
197
+ children
198
+ }, key)]
199
+ })
200
+ })] });
201
+ }
96
202
  /**
97
203
  * A button.
98
204
  *
205
+ * A label that changes (a different text, or a different icon) crossfades, and
206
+ * the button's width springs to fit it. While that runs, the OUTGOING label is
207
+ * still in the DOM for about 200ms, `aria-hidden` and out of flow, so read a
208
+ * button's text after the change settles, and do not put `id`s, inputs or
209
+ * anything stateful inside a label that changes. Text that changes constantly,
210
+ * such as a live counter, crossfades every time it does.
211
+ *
99
212
  * `size="icon"` renders a square button with no visible text, so it **must**
100
213
  * carry an `aria-label` -- otherwise it has no accessible name and fails axe's
101
214
  * `button-name` rule. The IconOnly story is the guard on that.
@@ -105,22 +218,54 @@ const buttonVariants = cva([
105
218
  * <Button startIcon={<SaveIcon aria-hidden />}>Save changes</Button>
106
219
  * <Button endIcon={<ChevronIcon aria-hidden />}>Continue</Button>
107
220
  * <Button variant="outline" size="icon" aria-label="Close"><XIcon /></Button>
221
+ * <Button href="/reports" variant="outlineNeutral">View reports</Button>
108
222
  */
109
- function Button$1({ className, variant, size, startIcon, endIcon, children, ...props }) {
110
- return /* @__PURE__ */ jsx(Button, {
111
- ...props,
112
- "data-slot": "button",
113
- className: cn(buttonVariants({
114
- variant,
115
- size
116
- }), className),
117
- children: startIcon === void 0 && endIcon === void 0 ? children : (renderProps) => /* @__PURE__ */ jsxs(Fragment, { children: [
223
+ const Button$1 = forwardRef(function Button$2({ className, variant, size, startIcon, endIcon, children, href, target, rel, download, ...props }, ref) {
224
+ const classes = cn(buttonVariants({
225
+ variant,
226
+ size
227
+ }), className);
228
+ const button = useAriaBusy(props.isPending === true);
229
+ const content = (renderProps) => /* @__PURE__ */ jsxs(ButtonContent, {
230
+ pending: renderProps.isPending,
231
+ children: [
118
232
  startIcon,
119
233
  typeof children === "function" ? children(renderProps) : children,
120
234
  endIcon
121
- ] })
235
+ ]
122
236
  });
123
- }
237
+ const { onPressStart } = props;
238
+ const pressStart = (event) => {
239
+ if (!anchorsLayer(props)) {
240
+ const target = event.target;
241
+ target.style.setProperty("--press-scale", pressScaleFor(target.offsetWidth));
242
+ }
243
+ onPressStart?.(event);
244
+ };
245
+ if (href !== void 0) return /* @__PURE__ */ jsx(Link, {
246
+ ref,
247
+ ...props,
248
+ onPressStart: pressStart,
249
+ href,
250
+ target,
251
+ rel,
252
+ download,
253
+ "data-slot": "button",
254
+ className: classes,
255
+ children: (renderProps) => content({
256
+ ...renderProps,
257
+ isPending: false
258
+ })
259
+ });
260
+ return /* @__PURE__ */ jsx(Button, {
261
+ ref: mergeRefs(ref, button),
262
+ ...props,
263
+ onPressStart: pressStart,
264
+ "data-slot": "button",
265
+ className: classes,
266
+ children: content
267
+ });
268
+ });
124
269
  //#endregion
125
270
  export { Button$1 as Button, buttonVariants };
126
271
 
@@ -1 +1 @@
1
- {"version":3,"file":"button.js","names":["Button","AriaButton"],"sources":["../../src/button/button.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type { ReactNode } from \"react\";\nimport { Button as AriaButton, type ButtonProps as AriaButtonProps } from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\n\n/**\n * Variant classes, exported so a consumer can reuse the exact classes we ship\n * rather than guessing at them. This is also the supported answer for the\n * link-shaped cases React Aria's Button cannot cover: it never renders an\n * anchor, so `<a className={buttonVariants({ variant: \"outline\" })}>` is how a\n * real navigation target gets button styling until the Link primitive lands.\n *\n * Ported from the legacy shadcn Button. The variant and size vocabulary is\n * deliberately identical so existing call sites migrate by rename, with six\n * corrections that the Alfred contracts require -- each marked below.\n *\n * Every string here is a STATIC literal. Tailwind scans source text, so a class\n * assembled at runtime is invisible to it and would simply never be emitted.\n *\n * State comes from React Aria data attributes rather than pseudo-classes:\n * data-[focus-visible] is keyboard focus only, and data-[pressed] covers mouse,\n * touch and keyboard uniformly. The original had no pressed state at all.\n */\nexport const buttonVariants = cva(\n [\n \"inline-flex items-center justify-center gap-(--density-control-gap) shrink-0 whitespace-nowrap\",\n // No leading-* here on purpose. tailwind-merge treats a later text-* class\n // as owning line-height (a Tailwind v3 assumption, where text-base carried\n // one), so a leading-* in the base string is silently STRIPPED by cn() once\n // a size variant adds text-base. It is not needed either: the control has a\n // fixed height and centres with flex, so line-height cannot overflow it.\n // A component with auto height must put leading-* AFTER its text-* class.\n \"rounded-md font-sans font-medium\",\n \"cursor-pointer select-none\",\n\n // font-sans above is part of the same problem: a <button> does not inherit\n // the page font, so without a reset it renders in the user-agent face\n // (Arial in Chromium) rather than the design system's. The same applies to\n // bg-transparent: an unstyled <button> paints the user-agent ButtonFace\n // grey -- which is what surfaces the moment a fill class fails to resolve,\n // and is why the paint stories compare against tokens rather than checking\n // for transparency.\n //\n // The reset in @seekalfred/styles is OPT-IN, so a consumer may never load\n // one. A bare <button> then keeps its user-agent border (2px, black, in\n // Chromium) and inline padding, which is a black box around every variant\n // that does not set a border of its own. The original inherited a zeroed\n // border from Tailwind Preflight in the app; a library component cannot\n // assume the consumer supplied one.\n \"border-(length:--border-width) border-transparent bg-transparent\",\n\n // Icon handling, ported verbatim: an svg child is sized unless the caller\n // already said otherwise, and never swallows the press.\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0\",\n \"[&_svg:not([class*='size-'])]:size-(--icon-sm)\",\n\n // CORRECTION 1 -- the original focused with `ring-[3px]`, which is a\n // box-shadow. Box-shadows are discarded under forced-colors, taking the\n // only focus indication with them, and axe has no rule that would catch it\n // (ARCHITECTURE section 5, rule 6). A real outline survives.\n //\n // outline-solid on the focus variant is load-bearing, not decoration.\n // `outline-none` sets BOTH outline-style:none and --tw-outline-style:none,\n // and Tailwind's outline-width utility resolves its style from that same\n // variable -- so the focus variant applied a width and a colour while\n // faithfully re-applying style:none, and painted nothing. The button was\n // keyboard-focusable with no visible ring. axe has no focus-indicator\n // rule, so every automated gate stayed green; the KeyboardFocus story now\n // asserts outlineStyle directly.\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\n // CORRECTION 2 -- `disabled:pointer-events-none` also kills the cursor, so\n // the control reads as inert rather than as unavailable. React Aria already\n // blocks activation; this only has to say so.\n \"data-[disabled]:cursor-not-allowed data-[disabled]:opacity-(--opacity-disabled)\",\n\n // React Aria's own pending state. The original had no equivalent.\n \"data-[pending]:cursor-progress\",\n ],\n {\n variants: {\n variant: {\n // CORRECTION 3 -- the legacy button filled this variant with the brand\n // gradient. It is flat here, because the brand system caps\n // --gradient-brand at one intentional element per view and keeps that\n // budget for the logo and progress bars: \"a gradient on every button\n // spends that budget everywhere. Ours is the flat primary.\"\n //\n // CORRECTION 4 -- the label is ink, not white. Brand orange #FF8431\n // carries white at 2.44:1; ink clears it at 8.35:1, which is the brand\n // system's own --text-on-orange rule. Its Button ships white only as a\n // dated, individually signed-off exception, and this library's contrast\n // contract is build-failing with no exemption mechanism to inherit it.\n //\n // CORRECTION 7 -- hover and pressed are their own TOKENS, not opacity\n // modifiers on the fill. `bg-primary/90` lightened the button toward the\n // page, which reads as receding rather than interactive and was nearly\n // invisible; it also made the label pair unmeasurable, because an alpha\n // composite depends on whatever happens to sit behind it. The brand system\n // steps DOWN its ramp instead -- 500 rest, 600 hover, 700 pressed -- and\n // every one of those fills is now in the contrast contract.\n primary: [\n \"bg-primary text-primary-foreground shadow-surface\",\n \"motion-safe:transition-colors motion-safe:duration-(--duration-fast) motion-safe:ease-standard\",\n \"data-[hovered]:bg-primary-hover\",\n \"data-[pressed]:bg-primary-pressed\",\n ],\n secondary: [\n \"bg-secondary text-secondary-foreground shadow-surface\",\n \"motion-safe:transition-colors\",\n \"data-[hovered]:bg-secondary-hover\",\n \"data-[pressed]:bg-secondary-pressed\",\n ],\n // primary-strong, never primary: the brand orange is 2.44:1 on the page\n // and cannot legally be a label OR a border. That is the whole reason\n // the token exists.\n outline: [\n \"bg-background text-primary-strong border-(length:--border-width) border-primary-strong shadow-surface\",\n \"motion-safe:transition-colors\",\n \"data-[hovered]:bg-muted\",\n \"data-[pressed]:bg-muted data-[pressed]:border-foreground\",\n ],\n /*\n * The NEUTRAL outline, and the product's single most-used button:\n * 99 call sites against 32 for the brand-coloured one.\n *\n * It exists because `outline` above is brand-toned -- orange border,\n * orange label -- which is what the product calls `secondaryOutline`.\n * Its plain `outline` is this: a grey border and ordinary body text,\n * for a button that must not compete with the primary action next to\n * it. One name could not cover both, and renaming `outline` would have\n * silently repainted every existing call site of ours instead.\n *\n * `border` rather than `primary-strong`: --color-border is the token\n * already held to the 3:1 non-text floor of WCAG 1.4.11, which is what\n * a border separating a control from the page has to clear.\n */\n outlineNeutral: [\n \"bg-background text-foreground border-(length:--border-width) border-border shadow-surface\",\n \"motion-safe:transition-colors\",\n \"data-[hovered]:bg-muted\",\n \"data-[pressed]:bg-muted data-[pressed]:border-foreground\",\n ],\n ghost: [\n \"bg-transparent text-primary-strong\",\n \"motion-safe:transition-colors\",\n \"data-[hovered]:bg-muted\",\n // CORRECTION 5 -- pressed must not be background-only. A ghost button\n // has no border at rest, so the pressed border is the non-colour cue\n // that survives forced-colors (rule 6 again).\n \"data-[pressed]:bg-muted data-[pressed]:border-primary-strong\",\n ],\n destructive: [\n \"bg-destructive text-destructive-foreground shadow-surface\",\n \"motion-safe:transition-colors\",\n \"data-[hovered]:bg-destructive-hover\",\n \"data-[pressed]:bg-destructive-pressed\",\n ],\n link: [\n \"bg-transparent text-primary-strong underline-offset-(--underline-offset)\",\n \"data-[hovered]:underline\",\n \"data-[pressed]:underline\",\n ],\n },\n size: {\n // Height, padding and label size all come from the control scale, so\n // data-density moves them together. The icon-only variant is square at\n // the md height. The legacy shadcn rule that tightened padding when a\n // child was an icon is gone: it needed three more magic numbers, the\n // brand system has no equivalent, and the gap token now owns icon\n // spacing.\n sm: \"h-(--density-control-h-sm) px-(--density-control-pad-sm) text-sm\",\n md: \"h-(--density-control-h-md) px-(--density-control-pad-md) text-base\",\n lg: \"h-(--density-control-h-lg) px-(--density-control-pad-lg) text-lg\",\n // text-base even though there is no visible label: without it the\n // user-agent default (13.33px in Chromium) leaks in and sizes any\n // em-based child against the wrong root.\n icon: \"size-(--density-control-h-md) px-0 text-base\",\n },\n },\n defaultVariants: {\n variant: \"primary\",\n size: \"md\",\n },\n },\n);\n\n/**\n * Props.\n *\n * CORRECTION 6 -- the legacy Button was a plain `<button>`, so it took\n * `onClick`, `disabled` and Radix's `asChild`. React Aria's Button has none of\n * those: `ButtonProps` explicitly omits `onClick` and `href`. The replacements\n * are `onPress`, `isDisabled`, and `buttonVariants()` on the consumer's own\n * element. See the migration table in the package README.\n *\n * Deliberately NOT intersected with ComponentPropsWithoutRef<\"button\">: that\n * would advertise disabled, tabIndex and role, which React Aria does not\n * implement and silently ignores. React Aria already forwards the native event\n * surface, so nothing is lost.\n */\nexport interface ButtonProps\n extends Omit<AriaButtonProps, \"className\" | \"style\">, VariantProps<typeof buttonVariants> {\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n /**\n * Icon rendered before the label.\n *\n * Mark the glyph `aria-hidden` -- the label is the accessible name, and a\n * second name source only makes the button announce twice. It is sized by the\n * base `[&_svg]` rule at `--icon-sm` unless it carries a `size-*` class of\n * its own, and spaced by `--density-control-gap`.\n *\n * Two named slots rather than one `icon` plus a position prop: a button can\n * legitimately carry a leading glyph AND a trailing affordance, which one\n * icon and a position cannot express.\n *\n * NOT the route for `size=\"icon\"`. An icon-only button passes its glyph as\n * `children`, which is what gives it its single centred child.\n */\n startIcon?: ReactNode;\n /** Icon rendered after the label. Same rules as {@link ButtonProps.startIcon}. */\n endIcon?: ReactNode;\n}\n\n/**\n * A button.\n *\n * `size=\"icon\"` renders a square button with no visible text, so it **must**\n * carry an `aria-label` -- otherwise it has no accessible name and fails axe's\n * `button-name` rule. The IconOnly story is the guard on that.\n *\n * @example\n * <Button onPress={save}>Save changes</Button>\n * <Button startIcon={<SaveIcon aria-hidden />}>Save changes</Button>\n * <Button endIcon={<ChevronIcon aria-hidden />}>Continue</Button>\n * <Button variant=\"outline\" size=\"icon\" aria-label=\"Close\"><XIcon /></Button>\n */\nexport function Button({\n className,\n variant,\n size,\n startIcon,\n endIcon,\n children,\n ...props\n}: ButtonProps) {\n return (\n <AriaButton\n {...props}\n data-slot=\"button\"\n className={cn(buttonVariants({ variant, size }), className)}\n >\n {/*\n * children passes through UNTOUCHED when neither slot is used, so a\n * button with no icon renders byte-identical DOM to before this prop\n * existed. That is what keeps this an additive change rather than a\n * structural one -- and the childOrder assertions in the stories are what\n * hold it true, since they read the button's direct children.\n *\n * The slot branch has to be a render function because React Aria's\n * children are ChildrenOrFunction<ButtonRenderProps>: a consumer may pass\n * a function to read isPressed or isPending, and wrapping the node\n * directly would silently render that function as nothing. The fragment\n * adds no element of its own.\n */}\n {startIcon === undefined && endIcon === undefined\n ? children\n : (renderProps) => (\n <>\n {startIcon}\n {typeof children === \"function\" ? children(renderProps) : children}\n {endIcon}\n </>\n )}\n </AriaButton>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AA0BA,MAAa,iBAAiB,IAC5B;CACE;CAOA;CACA;CAgBA;CAIA;CACA;CAeA;CACA;CACA;CACA;CACA;CAKA;CAGA;AACF,GACA;CACE,UAAU;EACR,SAAS;GAoBP,SAAS;IACP;IACA;IACA;IACA;GACF;GACA,WAAW;IACT;IACA;IACA;IACA;GACF;GAIA,SAAS;IACP;IACA;IACA;IACA;GACF;GAgBA,gBAAgB;IACd;IACA;IACA;IACA;GACF;GACA,OAAO;IACL;IACA;IACA;IAIA;GACF;GACA,aAAa;IACX;IACA;IACA;IACA;GACF;GACA,MAAM;IACJ;IACA;IACA;GACF;EACF;EACA,MAAM;GAOJ,IAAI;GACJ,IAAI;GACJ,IAAI;GAIJ,MAAM;EACR;CACF;CACA,iBAAiB;EACf,SAAS;EACT,MAAM;CACR;AACF,CACF;;;;;;;;;;;;;;AAqDA,SAAgBA,SAAO,EACrB,WACA,SACA,MACA,WACA,SACA,UACA,GAAG,SACW;CACd,OACE,oBAACC,QAAD;EACE,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,eAAe;GAAE;GAAS;EAAK,CAAC,GAAG,SAAS;EAezD,UAAA,cAAc,KAAA,KAAa,YAAY,KAAA,IACpC,YACC,gBACC,qBAAA,UAAA,EAAA,UAAA;GACG;GACA,OAAO,aAAa,aAAa,SAAS,WAAW,IAAI;GACzD;EACD,EAAA,CAAA;CAEE,CAAA;AAEhB"}
1
+ {"version":3,"file":"button.js","names":["Button","AriaLink","AriaButton"],"sources":["../../src/button/button.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport {\n forwardRef,\n isValidElement,\n useCallback,\n useRef,\n useState,\n type ForwardedRef,\n type ReactNode,\n type RefObject,\n} from \"react\";\nimport {\n Button as AriaButton,\n Link as AriaLink,\n type ButtonProps as AriaButtonProps,\n type ButtonRenderProps,\n type LinkProps as AriaLinkProps,\n type PressEvent,\n} from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { mergeRefs } from \"../lib/merge-refs.js\";\nimport {\n anchorsLayer,\n pressScaleFor,\n useAriaBusy,\n useExitBackstop,\n useLeavingItems,\n useMorphWidth,\n type Rect,\n} from \"../lib/motion.js\";\n\n/**\n * Variant classes, exported so a consumer can reuse the exact classes we ship\n * rather than guessing at them.\n *\n * NOT the answer for a link-shaped button any more: pass `href` to Button. On a\n * consumer's own <a> these classes get none of their states -- hover, press and\n * the focus ring all key on React Aria data attributes that a plain anchor never\n * receives, and the base sets outline-none, so keyboard focus was invisible.\n *\n * Ported from the legacy shadcn Button. The variant and size vocabulary is\n * deliberately identical so existing call sites migrate by rename, with six\n * corrections that the Alfred contracts require -- each marked below.\n *\n * Every string here is a STATIC literal. Tailwind scans source text, so a class\n * assembled at runtime is invisible to it and would simply never be emitted.\n *\n * State comes from React Aria data attributes rather than pseudo-classes:\n * data-[focus-visible] is keyboard focus only, and data-[pressed] covers mouse,\n * touch and keyboard uniformly. The original had no pressed state at all.\n */\n/*\n * The press: the button scales down while pressed (see onPressStart for the\n * scale, which depends on width) and springs back on release. Anchors of\n * floating layers never scale: a menu that opens on pointerdown would measure a\n * shrunken trigger.\n */\nconst pressScale =\n \"motion-safe:data-[pressed]:scale-(--press-scale) [&:is([aria-haspopup]:not([aria-haspopup=false]),[data-state],[role=combobox])]:scale-none!\";\n\nexport const buttonVariants = cva(\n [\n \"inline-flex items-center justify-center gap-(--density-control-gap) shrink-0 whitespace-nowrap\",\n // No leading-* here on purpose. tailwind-merge treats a later text-* class\n // as owning line-height (a Tailwind v3 assumption, where text-base carried\n // one), so a leading-* in the base string is silently STRIPPED by cn() once\n // a size variant adds text-base. It is not needed either: the control has a\n // fixed height and centres with flex, so line-height cannot overflow it.\n // A component with auto height must put leading-* AFTER its text-* class.\n \"rounded-md font-sans font-medium\",\n \"cursor-pointer select-none\",\n\n // font-sans above is part of the same problem: a <button> does not inherit\n // the page font, so without a reset it renders in the user-agent face\n // (Arial in Chromium) rather than the design system's. The same applies to\n // bg-transparent: an unstyled <button> paints the user-agent ButtonFace\n // grey -- which is what surfaces the moment a fill class fails to resolve,\n // and is why the paint stories compare against tokens rather than checking\n // for transparency.\n //\n // The reset in @seekalfred/styles is OPT-IN, so a consumer may never load\n // one. A bare <button> then keeps its user-agent border (2px, black, in\n // Chromium) and inline padding, which is a black box around every variant\n // that does not set a border of its own. The original inherited a zeroed\n // border from Tailwind Preflight in the app; a library component cannot\n // assume the consumer supplied one.\n \"border-(length:--border-width) border-transparent bg-transparent\",\n\n // Icon handling, ported verbatim: an svg child is sized unless the caller\n // already said otherwise, and never swallows the press.\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0\",\n \"[&_svg:not([class*='size-'])]:size-(--icon-sm)\",\n\n // CORRECTION 1 -- the original focused with `ring-[3px]`, which is a\n // box-shadow. Box-shadows are discarded under forced-colors, taking the\n // only focus indication with them, and axe has no rule that would catch it\n // (ARCHITECTURE section 5, rule 6). A real outline survives.\n //\n // outline-solid on the focus variant is load-bearing, not decoration.\n // `outline-none` sets BOTH outline-style:none and --tw-outline-style:none,\n // and Tailwind's outline-width utility resolves its style from that same\n // variable -- so the focus variant applied a width and a colour while\n // faithfully re-applying style:none, and painted nothing. The button was\n // keyboard-focusable with no visible ring. axe has no focus-indicator\n // rule, so every automated gate stayed green; the KeyboardFocus story now\n // asserts outlineStyle directly.\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\n // CORRECTION 2 -- `disabled:pointer-events-none` also kills the cursor, so\n // the control reads as inert rather than as unavailable. React Aria already\n // blocks activation; this only has to say so.\n // The opacity half of the disabled look moved into each variant: the\n // outlined and ghost buttons stay fully opaque and mute their colours\n // instead, which a single base opacity cannot express.\n \"data-[disabled]:cursor-not-allowed\",\n \"[-webkit-tap-highlight-color:transparent]\",\n\n // React Aria's own pending state. The original had no equivalent.\n \"data-[pending]:cursor-progress\",\n ],\n {\n variants: {\n variant: {\n // CORRECTION 3 -- the legacy button filled this variant with the brand\n // gradient. It is flat here, because the brand system caps\n // --gradient-brand at one intentional element per view and keeps that\n // budget for the logo and progress bars: \"a gradient on every button\n // spends that budget everywhere. Ours is the flat primary.\"\n //\n // CORRECTION 4 -- the label is ink, not white. Brand orange #FF8431\n // carries white at 2.44:1; ink clears it at 8.35:1, which is the brand\n // system's own --text-on-orange rule. Its Button ships white only as a\n // dated, individually signed-off exception, and this library's contrast\n // contract is build-failing with no exemption mechanism to inherit it.\n //\n // CORRECTION 7 (reversed by decision) -- hover and pressed used to be\n // their own TOKENS (500 rest, 600 hover, 700 pressed), because an\n // opacity modifier lightens the button toward the page, which reads as\n // receding, and makes the label pair unmeasurable: an alpha composite\n // depends on whatever sits behind it. They are now opacity steps (.91,\n // .84) plus the resting shadow, by explicit decision, so the hover and\n // pressed fills are no longer used here. The contrast contract still\n // lists them (contrast-pairs.json) but cannot express an alpha, so the\n // faded states were measured by hand: ink on orange, both faded over\n // the page, is 6.54:1 at .84 on the light page and 6.08:1 on the dark\n // one (8.35:1 at rest), all above the 4.5:1 floor.\n primary: [\n \"bg-primary text-primary-foreground shadow-surface\",\n \"motion-safe:button-motion\",\n pressScale,\n \"data-[hovered]:opacity-91 data-[hovered]:shadow-resting\",\n \"data-[pressed]:opacity-84\",\n \"data-[disabled]:opacity-(--opacity-button-disabled)\",\n ],\n secondary: [\n \"bg-secondary text-secondary-foreground shadow-surface\",\n \"motion-safe:button-motion\",\n pressScale,\n \"data-[hovered]:bg-secondary-hover data-[hovered]:shadow-resting\",\n \"data-[pressed]:bg-secondary-pressed\",\n // A disabled outlined button keeps its shape and mutes its\n // label, so it reads as a button that is off, not as plain text.\n \"data-[disabled]:opacity-100 data-[disabled]:bg-muted data-[disabled]:text-muted-foreground data-[disabled]:border-border\",\n ],\n // primary-strong, never primary: the brand orange is 2.44:1 on the page\n // and cannot legally be a label OR a border. That is the whole reason\n // the token exists.\n outline: [\n \"bg-background text-primary-strong border-(length:--border-width) border-primary-strong shadow-surface\",\n \"motion-safe:button-motion\",\n pressScale,\n \"data-[hovered]:bg-muted data-[hovered]:shadow-resting\",\n \"data-[pressed]:bg-muted data-[pressed]:border-foreground\",\n \"data-[disabled]:opacity-100 data-[disabled]:bg-muted data-[disabled]:text-muted-foreground data-[disabled]:border-border\",\n ],\n /*\n * The NEUTRAL outline, and the product's single most-used button:\n * 99 call sites against 32 for the brand-coloured one.\n *\n * It exists because `outline` above is brand-toned -- orange border,\n * orange label -- which is what the product calls `secondaryOutline`.\n * Its plain `outline` is this: a grey border and ordinary body text,\n * for a button that must not compete with the primary action next to\n * it. One name could not cover both, and renaming `outline` would have\n * silently repainted every existing call site of ours instead.\n *\n * `border` rather than `primary-strong`: --color-border is the token\n * already held to the 3:1 non-text floor of WCAG 1.4.11, which is what\n * a border separating a control from the page has to clear.\n */\n outlineNeutral: [\n \"bg-background text-foreground border-(length:--border-width) border-border shadow-surface\",\n \"motion-safe:button-motion\",\n pressScale,\n \"data-[hovered]:bg-muted data-[hovered]:shadow-resting\",\n \"data-[pressed]:bg-muted data-[pressed]:border-foreground\",\n \"data-[disabled]:opacity-100 data-[disabled]:bg-muted data-[disabled]:text-muted-foreground\",\n ],\n ghost: [\n \"bg-transparent text-primary-strong\",\n \"motion-safe:button-motion\",\n pressScale,\n \"data-[hovered]:bg-muted\",\n \"data-[disabled]:opacity-100 data-[disabled]:text-muted-foreground\",\n // CORRECTION 5 -- pressed must not be background-only. A ghost button\n // has no border at rest, so the pressed border is the non-colour cue\n // that survives forced-colors (rule 6 again).\n \"data-[pressed]:bg-muted data-[pressed]:border-primary-strong\",\n ],\n destructive: [\n \"bg-destructive text-destructive-foreground shadow-surface\",\n \"motion-safe:button-motion\",\n pressScale,\n \"data-[hovered]:bg-destructive-hover\",\n \"data-[pressed]:bg-destructive-pressed\",\n // Muted fill, label tinted toward destructive.\n \"data-[disabled]:opacity-100 data-[disabled]:bg-muted data-[disabled]:border-border data-[disabled]:text-[color-mix(in_oklab,var(--color-destructive)_55%,var(--color-muted))]\",\n ],\n link: [\n \"bg-transparent text-primary-strong underline-offset-(--underline-offset)\",\n \"motion-safe:button-motion\",\n \"data-[disabled]:opacity-(--opacity-button-disabled)\",\n \"data-[hovered]:underline\",\n \"data-[pressed]:underline\",\n ],\n },\n size: {\n // Height, padding and label size all come from the control scale, so\n // data-density moves them together. The icon-only variant is square at\n // the md height. The legacy shadcn rule that tightened padding when a\n // child was an icon is gone: it needed three more magic numbers, the\n // brand system has no equivalent, and the gap token now owns icon\n // spacing.\n sm: \"h-(--density-control-h-sm) px-(--density-control-pad-sm) text-sm\",\n md: \"h-(--density-control-h-md) px-(--density-control-pad-md) text-base\",\n lg: \"h-(--density-control-h-lg) px-(--density-control-pad-lg) text-lg\",\n // text-base even though there is no visible label: without it the\n // user-agent default (13.33px in Chromium) leaks in and sizes any\n // em-based child against the wrong root.\n icon: \"size-(--density-control-h-md) px-0 text-base\",\n },\n },\n defaultVariants: {\n variant: \"primary\",\n size: \"md\",\n },\n },\n);\n\n/*\n * The label motion, without an animation library.\n *\n * A presence library would keep the outgoing label mounted for the crossfade\n * and drive a motion value for the width morph. CSS cannot keep an outgoing\n * label mounted or observe its own content width, so those two jobs are the\n * plain React below; every animation itself is a CSS keyframe or transition\n * (@seekalfred/styles motion.css, the button-* utilities in styles.css, the\n * --animate-* tokens).\n */\n\n/** A key for the label content: text plus element names, so a new label or icon crossfades while prop-only updates stay in place. */\nfunction labelKey(node: ReactNode): string {\n if (node == null || typeof node === \"boolean\") return \"\";\n if (typeof node === \"string\" || typeof node === \"number\" || typeof node === \"bigint\") return String(node);\n if (Array.isArray(node)) return node.map(labelKey).join(\"\");\n if (!isValidElement(node)) return \"\";\n const type = node.type as string | { displayName?: string; name?: string };\n return \"<\" + (typeof type === \"string\" ? type : (type?.displayName ?? type?.name ?? \"\")) + \">\" + labelKey((node.props as { children?: ReactNode }).children);\n}\n\n/** A label with no text once element names are stripped is an icon, which scales instead of rising. */\nconst isIconKey = (key: string) => !/\\S/.test(key.replace(/<[^>]*>/g, \"\"));\n\ninterface LeavingLabel {\n id: number;\n node: ReactNode;\n icon: boolean;\n rect: Rect;\n}\n\nfunction LeavingPhase({ phase, onDone }: { phase: LeavingLabel; onDone: (id: number) => void }) {\n const { id } = phase;\n const remove = useCallback(() => onDone(id), [onDone, id]);\n useExitBackstop(true, remove);\n return (\n <span\n aria-hidden=\"true\"\n onAnimationEnd={(event) => {\n if (event.target === event.currentTarget) remove();\n }}\n style={phase.rect}\n className={cn(\n \"absolute inline-flex items-center justify-center gap-(--density-control-gap) whitespace-nowrap [&_svg]:shrink-0\",\n phase.icon\n ? \"motion-safe:animate-glyph-out motion-reduce:animate-fade-out\"\n : \"motion-safe:animate-label-out motion-reduce:animate-fade-out\",\n )}\n >\n {phase.node}\n </span>\n );\n}\n\n/** The loading spinner: it scales in on the snappy spring, and out smaller with a little blur. Nothing animates if it is there on first mount. */\nfunction Loader({ show }: { show: boolean }) {\n const [initial] = useState(show);\n const [rendered, setRendered] = useState(show);\n const [animated, setAnimated] = useState(false);\n const leaving = rendered && !show;\n if (show && !rendered) setRendered(true);\n if (!animated && show !== initial) setAnimated(true);\n const unmount = useCallback(() => setRendered(false), []);\n useExitBackstop(leaving, unmount);\n if (!rendered) return null;\n return (\n <span\n aria-hidden=\"true\"\n onAnimationEnd={(event) => {\n if (leaving && event.target === event.currentTarget) unmount();\n }}\n className={cn(\n \"absolute top-1/2 left-1/2 -m-2 grid size-4\",\n leaving\n ? \"motion-safe:animate-loader-out motion-reduce:animate-fade-out\"\n : animated && \"motion-safe:animate-loader-in motion-reduce:animate-fade-in\",\n )}\n >\n <span className=\"rounded-circle border-(length:--border-width-spinner) border-current border-r-transparent motion-safe:animate-spin-fast\" />\n </span>\n );\n}\n\n/**\n * The button's inside: a loader, and a slot that holds the current label.\n *\n * A new label (a different text, or a different icon) mounts under a new key and\n * rises in while the old one is popped out of flow where it was and lifts away.\n * Nothing animates on first mount, and a prop-only update (same key) stays in\n * place.\n */\nfunction ButtonContent({ children, pending }: { children: ReactNode; pending: boolean }) {\n const key = labelKey(children);\n const icon = isIconKey(key);\n const contentRef = useRef<HTMLSpanElement>(null);\n const phaseRef = useRef<HTMLSpanElement>(null);\n\n useMorphWidth(contentRef, key);\n const { leaving, changed, remove } = useLeavingItems(key, { node: children, icon }, () => phaseRef.current);\n\n return (\n <>\n <Loader show={pending} />\n <span className={cn(\"button-slot\", pending && \"opacity-0\")}>\n <span ref={contentRef} className=\"inline-flex flex-none items-center\">\n {leaving.map((item) => (\n <LeavingPhase key={item.id} phase={{ id: item.id, node: item.value.node, icon: item.value.icon, rect: item.rect }} onDone={remove} />\n ))}\n <span\n key={key}\n ref={phaseRef}\n data-slot=\"button-label\"\n className={cn(\n \"inline-flex items-center justify-center gap-(--density-control-gap) whitespace-nowrap [&_svg]:shrink-0\",\n changed &&\n (icon\n ? \"motion-safe:animate-glyph-in motion-reduce:animate-fade-in\"\n : \"motion-safe:animate-label-in motion-reduce:animate-fade-in\"),\n )}\n >\n {children}\n </span>\n </span>\n </span>\n </>\n );\n}\n\n/**\n * Props.\n *\n * CORRECTION 6 -- the legacy Button was a plain `<button>`, so it took\n * `onClick`, `disabled` and Radix's `asChild`. React Aria's Button has none of\n * those: `ButtonProps` explicitly omits `onClick` and `href`. The replacements\n * are `onPress`, `isDisabled`, and `href` -- which renders React Aria's Link in\n * place of its Button. See the migration table in the package README.\n *\n * Deliberately NOT intersected with ComponentPropsWithoutRef<\"button\">: that\n * would advertise disabled, tabIndex and role, which React Aria does not\n * implement and silently ignores. React Aria already forwards the native event\n * surface, so nothing is lost.\n */\nexport interface ButtonProps\n extends Omit<AriaButtonProps, \"className\" | \"style\">, VariantProps<typeof buttonVariants> {\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n /**\n * Icon rendered before the label.\n *\n * Mark the glyph `aria-hidden` -- the label is the accessible name, and a\n * second name source only makes the button announce twice. It is sized by the\n * base `[&_svg]` rule at `--icon-sm` unless it carries a `size-*` class of\n * its own, and spaced by `--density-control-gap`.\n *\n * Two named slots rather than one `icon` plus a position prop: a button can\n * legitimately carry a leading glyph AND a trailing affordance, which one\n * icon and a position cannot express.\n *\n * NOT the route for `size=\"icon\"`. An icon-only button passes its glyph as\n * `children`, which is what gives it its single centred child.\n */\n startIcon?: ReactNode;\n /** Icon rendered after the label. Same rules as {@link ButtonProps.startIcon}. */\n endIcon?: ReactNode;\n /**\n * Makes the button a link. It renders React Aria's `Link` -- a real `<a>`,\n * announced as a link -- with exactly the classes a button of the same\n * `variant` and `size` gets, and the same hover, press and focus-ring states.\n *\n * Use it whenever activating the control navigates. A button that navigates\n * cannot be opened in a new tab, is announced as the wrong role, and is\n * skipped by a screen reader's list of links.\n *\n * Client-side routing needs React Aria's `RouterProvider` mounted once above\n * it; without one, an `href` is an ordinary page load, which is also what an\n * external or download link wants.\n *\n * Button-only props have nothing to act on here: `type`, `form*` and\n * `isPending` are ignored, and the `ref` receives the `<a>`.\n */\n href?: AriaLinkProps[\"href\"];\n /** With `href` only: where to open it, e.g. `\"_blank\"`. */\n target?: AriaLinkProps[\"target\"];\n /** With `href` only. Give `\"noreferrer\"` alongside `target=\"_blank\"` to an external site. */\n rel?: AriaLinkProps[\"rel\"];\n /** With `href` only: download the target instead of opening it, optionally under this file name. */\n download?: AriaLinkProps[\"download\"];\n}\n\n/**\n * A button.\n *\n * A label that changes (a different text, or a different icon) crossfades, and\n * the button's width springs to fit it. While that runs, the OUTGOING label is\n * still in the DOM for about 200ms, `aria-hidden` and out of flow, so read a\n * button's text after the change settles, and do not put `id`s, inputs or\n * anything stateful inside a label that changes. Text that changes constantly,\n * such as a live counter, crossfades every time it does.\n *\n * `size=\"icon\"` renders a square button with no visible text, so it **must**\n * carry an `aria-label` -- otherwise it has no accessible name and fails axe's\n * `button-name` rule. The IconOnly story is the guard on that.\n *\n * @example\n * <Button onPress={save}>Save changes</Button>\n * <Button startIcon={<SaveIcon aria-hidden />}>Save changes</Button>\n * <Button endIcon={<ChevronIcon aria-hidden />}>Continue</Button>\n * <Button variant=\"outline\" size=\"icon\" aria-label=\"Close\"><XIcon /></Button>\n * <Button href=\"/reports\" variant=\"outlineNeutral\">View reports</Button>\n */\nexport const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(\n { className, variant, size, startIcon, endIcon, children, href, target, rel, download, ...props },\n ref,\n) {\n const classes = cn(buttonVariants({ variant, size }), className);\n\n const button = useAriaBusy(props.isPending === true);\n\n /*\n * children passes through UNTOUCHED when neither slot is used, so a button\n * with no icon renders byte-identical DOM to before this prop existed. That is\n * what keeps this an additive change rather than a structural one -- and the\n * childOrder assertions in the stories are what hold it true, since they read\n * the button's direct children.\n *\n * The slot branch has to be a render function because React Aria's children\n * are ChildrenOrFunction<ButtonRenderProps>: a consumer may pass a function to\n * read isPressed or isPending, and wrapping the node directly would silently\n * render that function as nothing. The fragment adds no element of its own.\n */\n const content = (renderProps: ButtonRenderProps & { defaultChildren: ReactNode | undefined }) => (\n <ButtonContent pending={renderProps.isPending}>\n {startIcon}\n {typeof children === \"function\" ? children(renderProps) : children}\n {endIcon}\n </ButtonContent>\n );\n\n const { onPressStart } = props;\n const pressStart = (event: PressEvent) => {\n if (!anchorsLayer(props)) {\n const target = event.target as HTMLElement;\n target.style.setProperty(\"--press-scale\", pressScaleFor(target.offsetWidth));\n }\n onPressStart?.(event);\n };\n\n if (href !== undefined) {\n /*\n * The same classes on React Aria's Link. It sets data-hovered,\n * data-pressed and data-focus-visible on the <a>, which is what every\n * state class above keys on -- nothing here is re-implemented.\n *\n * Button-only props need no stripping: Link runs its props through\n * filterDOMProps, so `type` never reaches the <a>, and `isPending` is not a\n * DOM prop. A link has no pending state, so a render-function child is told\n * it is not pending rather than handed an object missing the field. The ref\n * is typed for the button this usually is; with href it receives the <a>,\n * which the TSDoc on `href` says.\n */\n return (\n <AriaLink\n ref={ref as unknown as ForwardedRef<HTMLAnchorElement>}\n {...(props as Omit<AriaLinkProps, \"className\" | \"children\">)}\n onPressStart={pressStart}\n href={href}\n target={target}\n rel={rel}\n download={download}\n data-slot=\"button\"\n className={classes}\n >\n {(renderProps) => content({ ...renderProps, isPending: false })}\n </AriaLink>\n );\n }\n\n return (\n <AriaButton ref={mergeRefs(ref, button as RefObject<HTMLButtonElement | null>)} {...props} onPressStart={pressStart} data-slot=\"button\" className={classes}>\n {content}\n </AriaButton>\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4DA,MAAM,aACJ;AAEF,MAAa,iBAAiB,IAC5B;CACE;CAOA;CACA;CAgBA;CAIA;CACA;CAeA;CACA;CACA;CACA;CACA;CAQA;CACA;CAGA;AACF,GACA;CACE,UAAU;EACR,SAAS;GAwBP,SAAS;IACP;IACA;IACA;IACA;IACA;IACA;GACF;GACA,WAAW;IACT;IACA;IACA;IACA;IACA;IAGA;GACF;GAIA,SAAS;IACP;IACA;IACA;IACA;IACA;IACA;GACF;GAgBA,gBAAgB;IACd;IACA;IACA;IACA;IACA;IACA;GACF;GACA,OAAO;IACL;IACA;IACA;IACA;IACA;IAIA;GACF;GACA,aAAa;IACX;IACA;IACA;IACA;IACA;IAEA;GACF;GACA,MAAM;IACJ;IACA;IACA;IACA;IACA;GACF;EACF;EACA,MAAM;GAOJ,IAAI;GACJ,IAAI;GACJ,IAAI;GAIJ,MAAM;EACR;CACF;CACA,iBAAiB;EACf,SAAS;EACT,MAAM;CACR;AACF,CACF;;AAcA,SAAS,SAAS,MAAyB;CACzC,IAAI,QAAQ,QAAQ,OAAO,SAAS,WAAW,OAAO;CACtD,IAAI,OAAO,SAAS,YAAY,OAAO,SAAS,YAAY,OAAO,SAAS,UAAU,OAAO,OAAO,IAAI;CACxG,IAAI,MAAM,QAAQ,IAAI,GAAG,OAAO,KAAK,IAAI,QAAQ,CAAC,CAAC,KAAK,EAAE;CAC1D,IAAI,CAAC,eAAe,IAAI,GAAG,OAAO;CAClC,MAAM,OAAO,KAAK;CAClB,OAAO,OAAO,OAAO,SAAS,WAAW,OAAQ,MAAM,eAAe,MAAM,QAAQ,MAAO,MAAM,SAAU,KAAK,MAAmC,QAAQ;AAC7J;;AAGA,MAAM,aAAa,QAAgB,CAAC,KAAK,KAAK,IAAI,QAAQ,YAAY,EAAE,CAAC;AASzE,SAAS,aAAa,EAAE,OAAO,UAAiE;CAC9F,MAAM,EAAE,OAAO;CACf,MAAM,SAAS,kBAAkB,OAAO,EAAE,GAAG,CAAC,QAAQ,EAAE,CAAC;CACzD,gBAAgB,MAAM,MAAM;CAC5B,OACE,oBAAC,QAAD;EACE,eAAY;EACZ,iBAAiB,UAAU;GACzB,IAAI,MAAM,WAAW,MAAM,eAAe,OAAO;EACnD;EACA,OAAO,MAAM;EACb,WAAW,GACT,mHACA,MAAM,OACF,iEACA,8DACN;EAEC,UAAA,MAAM;CACH,CAAA;AAEV;;AAGA,SAAS,OAAO,EAAE,QAA2B;CAC3C,MAAM,CAAC,WAAW,SAAS,IAAI;CAC/B,MAAM,CAAC,UAAU,eAAe,SAAS,IAAI;CAC7C,MAAM,CAAC,UAAU,eAAe,SAAS,KAAK;CAC9C,MAAM,UAAU,YAAY,CAAC;CAC7B,IAAI,QAAQ,CAAC,UAAU,YAAY,IAAI;CACvC,IAAI,CAAC,YAAY,SAAS,SAAS,YAAY,IAAI;CACnD,MAAM,UAAU,kBAAkB,YAAY,KAAK,GAAG,CAAC,CAAC;CACxD,gBAAgB,SAAS,OAAO;CAChC,IAAI,CAAC,UAAU,OAAO;CACtB,OACE,oBAAC,QAAD;EACE,eAAY;EACZ,iBAAiB,UAAU;GACzB,IAAI,WAAW,MAAM,WAAW,MAAM,eAAe,QAAQ;EAC/D;EACA,WAAW,GACT,8CACA,UACI,kEACA,YAAY,6DAClB;EAEA,UAAA,oBAAC,QAAD,EAAM,WAAU,0HAA2H,CAAA;CACvI,CAAA;AAEV;;;;;;;;;AAUA,SAAS,cAAc,EAAE,UAAU,WAAsD;CACvF,MAAM,MAAM,SAAS,QAAQ;CAC7B,MAAM,OAAO,UAAU,GAAG;CAC1B,MAAM,aAAa,OAAwB,IAAI;CAC/C,MAAM,WAAW,OAAwB,IAAI;CAE7C,cAAc,YAAY,GAAG;CAC7B,MAAM,EAAE,SAAS,SAAS,WAAW,gBAAgB,KAAK;EAAE,MAAM;EAAU;CAAK,SAAS,SAAS,OAAO;CAE1G,OACE,qBAAA,YAAA,EAAA,UAAA,CACE,oBAAC,QAAD,EAAQ,MAAM,QAAU,CAAA,GACxB,oBAAC,QAAD;EAAM,WAAW,GAAG,eAAe,WAAW,WAAW;EACvD,UAAA,qBAAC,QAAD;GAAM,KAAK;GAAY,WAAU;GAAjC,UAAA,CACG,QAAQ,KAAK,SACZ,oBAAC,cAAD;IAA4B,OAAO;KAAE,IAAI,KAAK;KAAI,MAAM,KAAK,MAAM;KAAM,MAAM,KAAK,MAAM;KAAM,MAAM,KAAK;IAAK;IAAG,QAAQ;GAAS,GAAjH,KAAK,EAA4G,CACrI,GACD,oBAAC,QAAD;IAEE,KAAK;IACL,aAAU;IACV,WAAW,GACT,0GACA,YACG,OACG,+DACA,6DACR;IAEC;GACG,GAZC,GAYD,CACF;;CACF,CAAA,CACN,EAAA,CAAA;AAEN;;;;;;;;;;;;;;;;;;;;;;AAoFA,MAAaA,WAAS,WAA2C,SAASA,SACxE,EAAE,WAAW,SAAS,MAAM,WAAW,SAAS,UAAU,MAAM,QAAQ,KAAK,UAAU,GAAG,SAC1F,KACA;CACA,MAAM,UAAU,GAAG,eAAe;EAAE;EAAS;CAAK,CAAC,GAAG,SAAS;CAE/D,MAAM,SAAS,YAAY,MAAM,cAAc,IAAI;CAcnD,MAAM,WAAW,gBACf,qBAAC,eAAD;EAAe,SAAS,YAAY;EAApC,UAAA;GACG;GACA,OAAO,aAAa,aAAa,SAAS,WAAW,IAAI;GACzD;EACY;;CAGjB,MAAM,EAAE,iBAAiB;CACzB,MAAM,cAAc,UAAsB;EACxC,IAAI,CAAC,aAAa,KAAK,GAAG;GACxB,MAAM,SAAS,MAAM;GACrB,OAAO,MAAM,YAAY,iBAAiB,cAAc,OAAO,WAAW,CAAC;EAC7E;EACA,eAAe,KAAK;CACtB;CAEA,IAAI,SAAS,KAAA,GAaX,OACE,oBAACC,MAAD;EACO;EACL,GAAK;EACL,cAAc;EACR;EACE;EACH;EACK;EACV,aAAU;EACV,WAAW;EAET,WAAA,gBAAgB,QAAQ;GAAE,GAAG;GAAa,WAAW;EAAM,CAAC;CACtD,CAAA;CAId,OACE,oBAACC,QAAD;EAAY,KAAK,UAAU,KAAK,MAA6C;EAAG,GAAI;EAAO,cAAc;EAAY,aAAU;EAAS,WAAW;EAChJ,UAAA;CACS,CAAA;AAEhB,CAAC"}
@@ -0,0 +1,76 @@
1
+ import { ReactNode } from "react";
2
+ //#region src/button-group/button-group.d.ts
3
+ export type ButtonGroupVariant = "outline" | "solid";
4
+ export type ButtonGroupSize = "sm" | "md";
5
+ export type ButtonGroupOrientation = "horizontal" | "vertical";
6
+ export interface ButtonGroupItem {
7
+ /** Stable key for the segment. */
8
+ id: string;
9
+ /** Visible text and the accessible name. Changing it crossfades in place, so "Share" can answer "Copied". */
10
+ label: string;
11
+ /** Every other label this segment can show, such as ["Copied"]. The segment sizes to the widest, so a label change never resizes it. */
12
+ reserve?: string[];
13
+ /** An icon before the label. Swapping it for another morphs; re-rendering the same icon does not. Mark it `aria-hidden`. */
14
+ icon?: ReactNode;
15
+ /** Shows only the icon; the label becomes the accessible name and the tooltip. */
16
+ iconOnly?: boolean;
17
+ /** Visible content in place of the label, such as a live value. The label stays the accessible name. */
18
+ content?: ReactNode;
19
+ onSelect?: () => void;
20
+ /** Renders the segment as a link. */
21
+ href?: string;
22
+ isDisabled?: boolean;
23
+ }
24
+ export interface ButtonGroupMenuItem {
25
+ id: string;
26
+ label: string;
27
+ /** An icon before the label. Mark it `aria-hidden`. */
28
+ icon?: ReactNode;
29
+ onSelect?: () => void;
30
+ href?: string;
31
+ isDisabled?: boolean;
32
+ /** Colours the item as a destructive action. */
33
+ destructive?: boolean;
34
+ }
35
+ export interface ButtonGroupMenu {
36
+ /** Accessible name and tooltip of the chevron segment, such as "More actions". */
37
+ label: string;
38
+ items: ButtonGroupMenuItem[];
39
+ }
40
+ export interface ButtonGroupProps {
41
+ items: ButtonGroupItem[];
42
+ /** Adds a trailing chevron segment that opens these actions in a menu aligned to the group's edge. */
43
+ menu?: ButtonGroupMenu;
44
+ /** Accessible name of the group, such as "Document actions". */
45
+ label: string;
46
+ /** @default "outline" */
47
+ variant?: ButtonGroupVariant;
48
+ /** @default "md" */
49
+ size?: ButtonGroupSize;
50
+ /** @default "horizontal" */
51
+ orientation?: ButtonGroupOrientation;
52
+ /** When the row does not fit its container, segments with an icon drop their label and keep the icon. Horizontal only. @default true */
53
+ collapseLabels?: boolean;
54
+ /** Disables every segment and the menu. */
55
+ isDisabled?: boolean;
56
+ /** Extra classes. Merged last, so they win conflicts with ours. */
57
+ className?: string;
58
+ }
59
+ /**
60
+ * Related actions joined into one surface: a shared border and radius, hairline
61
+ * dividers, no gaps. One soft highlight glides between segments under the
62
+ * pointer or keyboard focus, the pressed segment answers in place, and an
63
+ * optional chevron segment opens more actions in a menu aligned to the group's
64
+ * edge.
65
+ *
66
+ * Tab visits every segment in order; the arrow keys along the orientation, Home
67
+ * and End also move between them. A segment's label can change in place ("Share"
68
+ * answers "Copied") without resizing it, and a label that changes right after its
69
+ * segment is pressed is announced once.
70
+ *
71
+ * @example
72
+ * <ButtonGroup label="Document actions" items={[{ id: "edit", label: "Edit" }, { id: "share", label: "Share", reserve: ["Copied"] }]} />
73
+ */
74
+ export declare function ButtonGroup({ items, menu, label, variant, size, orientation, collapseLabels, isDisabled, className }: ButtonGroupProps): import("react").JSX.Element;
75
+ //#endregion
76
+ //# sourceMappingURL=button-group.d.ts.map