@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,6 +1,6 @@
1
1
  import { VariantProps } from "class-variance-authority";
2
- import { DisclosureGroupProps, DisclosurePanelProps, DisclosureProps } from "react-aria-components";
3
2
  import { ReactNode } from "react";
3
+ import { DisclosureGroupProps, DisclosurePanelProps, DisclosureProps } from "react-aria-components";
4
4
  //#region src/accordion/accordion.d.ts
5
5
  /**
6
6
  * Accordion, ported from the legacy shadcn/Radix component onto React Aria's
@@ -50,7 +50,7 @@ export interface AccordionProps extends Omit<DisclosureGroupProps, "className" |
50
50
  /** Extra classes. Merged last, so they win conflicts with ours. */
51
51
  className?: string;
52
52
  }
53
- export declare function Accordion({ className, ...props }: AccordionProps): import("react").JSX.Element;
53
+ export declare const Accordion: import("react").ForwardRefExoticComponent<AccordionProps & import("react").RefAttributes<HTMLDivElement>>;
54
54
  /**
55
55
  * One collapsible section.
56
56
  *
@@ -60,7 +60,7 @@ export interface AccordionItemProps extends Omit<DisclosureProps, "className" |
60
60
  /** Extra classes. Merged last, so they win conflicts with ours. */
61
61
  className?: string;
62
62
  }
63
- export declare function AccordionItem({ className, ...props }: AccordionItemProps): import("react").JSX.Element;
63
+ export declare const AccordionItem: import("react").ForwardRefExoticComponent<AccordionItemProps & import("react").RefAttributes<HTMLDivElement>>;
64
64
  /**
65
65
  * The header button that expands its item.
66
66
  *
@@ -91,7 +91,7 @@ export interface AccordionTriggerProps extends VariantProps<typeof accordionTrig
91
91
  */
92
92
  headingLevel?: number;
93
93
  }
94
- export declare function AccordionTrigger({ children, icon, className, headingLevel }: AccordionTriggerProps): import("react").JSX.Element;
94
+ export declare const AccordionTrigger: import("react").ForwardRefExoticComponent<AccordionTriggerProps & import("react").RefAttributes<HTMLButtonElement>>;
95
95
  /**
96
96
  * The content revealed when its item expands.
97
97
  *
@@ -102,6 +102,6 @@ export interface AccordionPanelProps extends Omit<DisclosurePanelProps, "classNa
102
102
  /** Extra classes. Merged last, so they win conflicts with ours. */
103
103
  className?: string;
104
104
  }
105
- export declare function AccordionPanel({ className, ...props }: AccordionPanelProps): import("react").JSX.Element;
105
+ export declare const AccordionPanel: import("react").ForwardRefExoticComponent<AccordionPanelProps & import("react").RefAttributes<HTMLDivElement>>;
106
106
  //#endregion
107
107
  //# sourceMappingURL=accordion.d.ts.map
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { cn } from "../lib/cn.js";
3
3
  import { cva } from "class-variance-authority";
4
+ import { forwardRef } from "react";
4
5
  import { Button, Disclosure, DisclosureGroup, DisclosurePanel, Heading } from "react-aria-components";
5
6
  import { jsx, jsxs } from "react/jsx-runtime";
6
7
  //#region src/accordion/accordion.tsx
@@ -43,41 +44,51 @@ const accordionTriggerVariants = cva([
43
44
  "[&_svg]:pointer-events-none [&_svg]:shrink-0",
44
45
  "[&_svg:not([class*='size-'])]:size-(--icon-sm)",
45
46
  "[&_svg]:text-muted-foreground",
46
- "[&_svg]:motion-safe:transition-transform [&_svg]:motion-safe:duration-(--duration-fast)"
47
+ "[&_svg]:motion-safe:transition-transform [&_svg]:motion-safe:duration-(--duration-snappy) [&_svg]:motion-safe:ease-snappy",
48
+ "[&_svg]:motion-safe:transition-[transform,color]"
47
49
  ]);
48
- const accordionPanelVariants = cva(["pb-(--density-control-pad-md) font-sans text-sm leading-(--leading-normal) text-foreground"]);
49
- function Accordion({ className, ...props }) {
50
+ const accordionPanelVariants = cva(["pb-(--density-control-pad-md) font-sans text-sm leading-(--leading-normal) text-foreground", "accordion-panel"]);
51
+ const Accordion = forwardRef(function Accordion({ className, ...props }, ref) {
50
52
  return /* @__PURE__ */ jsx(DisclosureGroup, {
53
+ ref,
51
54
  ...props,
52
55
  "data-slot": "accordion",
53
56
  className: cn(accordionVariants(), className)
54
57
  });
55
- }
56
- function AccordionItem({ className, ...props }) {
58
+ });
59
+ const AccordionItem = forwardRef(function AccordionItem({ className, ...props }, ref) {
57
60
  return /* @__PURE__ */ jsx(Disclosure, {
61
+ ref,
58
62
  ...props,
59
63
  "data-slot": "accordion-item",
60
64
  className: cn(accordionItemVariants(), className)
61
65
  });
62
- }
63
- function AccordionTrigger({ children, icon, className, headingLevel = 3 }) {
66
+ });
67
+ const AccordionTrigger = forwardRef(function AccordionTrigger({ children, icon, className, headingLevel = 3 }, ref) {
64
68
  return /* @__PURE__ */ jsx(Heading, {
65
69
  level: headingLevel,
66
70
  children: /* @__PURE__ */ jsxs(Button, {
71
+ ref,
67
72
  slot: "trigger",
68
73
  "data-slot": "accordion-trigger",
69
74
  className: cn(accordionTriggerVariants(), className),
70
75
  children: [children, icon]
71
76
  })
72
77
  });
73
- }
74
- function AccordionPanel({ className, ...props }) {
78
+ });
79
+ const AccordionPanel = forwardRef(function AccordionPanel({ className, children, ...props }, ref) {
75
80
  return /* @__PURE__ */ jsx(DisclosurePanel, {
81
+ ref,
76
82
  ...props,
77
83
  "data-slot": "accordion-panel",
78
- className: cn(accordionPanelVariants(), className)
84
+ className: cn(accordionPanelVariants(), className),
85
+ children: /* @__PURE__ */ jsx("div", {
86
+ "data-slot": "accordion-panel-content",
87
+ className: "accordion-panel-content",
88
+ children
89
+ })
79
90
  });
80
- }
91
+ });
81
92
  //#endregion
82
93
  export { Accordion, AccordionItem, AccordionPanel, AccordionTrigger, accordionItemVariants, accordionPanelVariants, accordionTriggerVariants, accordionVariants };
83
94
 
@@ -1 +1 @@
1
- {"version":3,"file":"accordion.js","names":["AriaDisclosureGroup","AriaDisclosure","AriaButton","AriaDisclosurePanel"],"sources":["../../src/accordion/accordion.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type { ReactNode } from \"react\";\nimport {\n Button as AriaButton,\n Disclosure as AriaDisclosure,\n DisclosureGroup as AriaDisclosureGroup,\n DisclosurePanel as AriaDisclosurePanel,\n Heading,\n type DisclosureGroupProps as AriaDisclosureGroupProps,\n type DisclosurePanelProps as AriaDisclosurePanelProps,\n type DisclosureProps as AriaDisclosureProps,\n} from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\n\n/**\n * Accordion, ported from the legacy shadcn/Radix component onto React Aria's\n * Disclosure primitives.\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 * State comes from React Aria data attributes (`data-expanded`,\n * `data-focus-visible`, `data-disabled`), never from pseudo-classes.\n *\n * ONE BEHAVIOUR IS DELIBERATELY DROPPED. Radix moved focus between headers\n * with Home/End and the arrow keys; React Aria's useDisclosure has no key\n * handling, so each trigger is a button in the tab order and arrows do\n * nothing. The ARIA APG lists arrow navigation for accordions as optional,\n * and hand-rolling roving focus would mean owning focus management that the\n * headless base deliberately does not. The TabReachesEveryTrigger story pins\n * this so it stays a decision rather than becoming a surprise.\n */\nexport const accordionVariants = cva(\"w-full\");\n\nexport const accordionItemVariants = cva([\n // The divider between items. border-b-0 on the last child rather than\n // border-t on siblings: it survives a single-item accordion, which the\n // sibling form does not.\n \"border-b-(length:--border-width) border-b-border last:border-b-0\",\n // The chevron rotation is authored HERE, not on the trigger: React Aria\n // puts data-expanded on the Disclosure, and CSS cannot look upward from\n // the button to its expanded ancestor.\n \"data-[expanded]:[&_[data-slot=accordion-trigger]_svg]:rotate-180\",\n]);\n\nexport const accordionTriggerVariants = cva([\n \"flex w-full items-center justify-between gap-(--density-control-gap)\",\n // text-start, never text-left. Logical properties are what make RTL work\n // without a second stylesheet (ARCHITECTURE section 5 rule 4).\n \"text-start text-sm leading-(--leading-normal) font-sans font-medium text-foreground\",\n \"rounded-md py-(--density-control-pad-md)\",\n \"cursor-pointer select-none\",\n\n // The reset in @seekalfred/styles is opt-in, so a consumer may load none.\n // A bare <button> then keeps the user-agent border, ButtonFace grey and\n // Arial -- the same defence the Button base carries, for the same reason.\n \"border-(length:--border-width) border-transparent bg-transparent\",\n\n // A real outline, not a ring. The legacy component focused with\n // ring-[3px], a box-shadow, which forced-colors discards entirely -- and\n // axe has no focus-indicator rule, so nothing would have caught it.\n // outline-solid is load-bearing: outline-none sets --tw-outline-style:none\n // and the width utility resolves its style from that same variable, so\n // without it the ring gets a width and a colour and paints nothing.\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 \"motion-safe:transition-colors motion-safe:duration-(--duration-fast) motion-safe:ease-standard\",\n \"data-[hovered]:underline\",\n \"data-[disabled]:cursor-not-allowed data-[disabled]:opacity-(--opacity-disabled)\",\n\n // The chevron. Sized from the icon scale unless the caller sized it, and\n // never a press target of its own.\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0\",\n \"[&_svg:not([class*='size-'])]:size-(--icon-sm)\",\n \"[&_svg]:text-muted-foreground\",\n // Rotation is the whole motion story for this component -- see the panel\n // note below for why the panel itself does not animate.\n \"[&_svg]:motion-safe:transition-transform [&_svg]:motion-safe:duration-(--duration-fast)\",\n]);\n\nexport const accordionPanelVariants = cva([\n // font-sans is not decoration. The panel is a plain <div> inheriting from\n // the consumer's body, and the library deliberately does not style the\n // page -- so without this the copy renders in the browser's default serif\n // while the trigger above it renders in the brand face. Caught by eye in\n // the browser, not by any assertion, which is why NoUaLeak now covers the\n // panel too.\n \"pb-(--density-control-pad-md) font-sans text-sm leading-(--leading-normal) text-foreground\",\n // No expand/collapse animation, deliberately. React Aria hides a collapsed\n // panel with the `hidden` attribute, which is display:none, and CSS cannot\n // transition out of display:none. Overriding [hidden] would restore the\n // animation by breaking the accessibility semantics that make hidden\n // correct -- the panel would stay in the accessibility tree while\n // collapsed. @starting-style with transition-behavior:allow-discrete could\n // do this properly once the browser floor allows it.\n]);\n\n/**\n * A grouping of collapsible sections.\n *\n * Expansion is KEYED, which is the sharpest break from the legacy component:\n * `type=\"single\" | \"multiple\"` becomes {@link AccordionProps.allowsMultipleExpanded},\n * and `value` / `defaultValue` strings become `expandedKeys` /\n * `defaultExpandedKeys` holding item ids. The change handler receives a\n * `Set`, not a string.\n *\n * The legacy `collapsible` prop has no equivalent and needs none: React\n * Aria's single-expand mode always allows collapsing the open item.\n *\n * @example\n * <Accordion defaultExpandedKeys={[\"billing\"]}>\n * <AccordionItem id=\"billing\">\n * <AccordionTrigger icon={<ChevronDown aria-hidden />}>Billing</AccordionTrigger>\n * <AccordionPanel>Invoices and payment methods.</AccordionPanel>\n * </AccordionItem>\n * </Accordion>\n */\nexport interface AccordionProps\n extends\n Omit<AriaDisclosureGroupProps, \"className\" | \"style\">,\n VariantProps<typeof accordionVariants> {\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nexport function Accordion({ className, ...props }: AccordionProps) {\n return (\n <AriaDisclosureGroup\n {...props}\n data-slot=\"accordion\"\n className={cn(accordionVariants(), className)}\n />\n );\n}\n\n/**\n * One collapsible section.\n *\n * `id` replaces the legacy `value`, and is what `expandedKeys` refers to.\n */\nexport interface AccordionItemProps\n extends\n Omit<AriaDisclosureProps, \"className\" | \"style\">,\n VariantProps<typeof accordionItemVariants> {\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nexport function AccordionItem({ className, ...props }: AccordionItemProps) {\n return (\n <AriaDisclosure\n {...props}\n data-slot=\"accordion-item\"\n className={cn(accordionItemVariants(), className)}\n />\n );\n}\n\n/**\n * The header button that expands its item.\n *\n * Rendered inside a {@link Heading} because screen-reader users navigate an\n * accordion by heading; that wrapper is what makes it usable rather than\n * merely operable.\n */\nexport interface AccordionTriggerProps extends VariantProps<typeof accordionTriggerVariants> {\n /** The header label. */\n children?: ReactNode;\n /**\n * The expand/collapse glyph, rendered after the label.\n *\n * Required, and supplied by the caller: this library ships no icon set, so\n * it stays icon-library agnostic. Mark the glyph `aria-hidden` -- the label\n * is the accessible name, and a second name source makes the trigger\n * announce twice. It is sized at `--icon-sm` unless it carries a `size-*`\n * class of its own, and rotates on expand.\n */\n icon: ReactNode;\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n /**\n * Heading level for the wrapper. Accordions appear at different depths, and\n * a fixed level would break the document outline wherever it is wrong.\n *\n * @default 3\n */\n headingLevel?: number;\n}\n\nexport function AccordionTrigger({\n children,\n icon,\n className,\n headingLevel = 3,\n}: AccordionTriggerProps) {\n return (\n <Heading level={headingLevel}>\n <AriaButton\n slot=\"trigger\"\n data-slot=\"accordion-trigger\"\n className={cn(accordionTriggerVariants(), className)}\n >\n {children}\n {icon}\n </AriaButton>\n </Heading>\n );\n}\n\n/**\n * The content revealed when its item expands.\n *\n * Named Panel rather than the legacy Content, matching React Aria and the\n * ARIA pattern.\n */\nexport interface AccordionPanelProps\n extends\n Omit<AriaDisclosurePanelProps, \"className\" | \"style\">,\n VariantProps<typeof accordionPanelVariants> {\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nexport function AccordionPanel({ className, ...props }: AccordionPanelProps) {\n return (\n <AriaDisclosurePanel\n {...props}\n data-slot=\"accordion-panel\"\n className={cn(accordionPanelVariants(), className)}\n />\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAqCA,MAAa,oBAAoB,IAAI,QAAQ;AAE7C,MAAa,wBAAwB,IAAI,CAIvC,oEAIA,kEACF,CAAC;AAED,MAAa,2BAA2B,IAAI;CAC1C;CAGA;CACA;CACA;CAKA;CAQA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CAIA;CACA;CACA;CAGA;AACF,CAAC;AAED,MAAa,yBAAyB,IAAI,CAOxC,4FAQF,CAAC;AA8BD,SAAgB,UAAU,EAAE,WAAW,GAAG,SAAyB;CACjE,OACE,oBAACA,iBAAD;EACE,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,kBAAkB,GAAG,SAAS;CAC7C,CAAA;AAEL;AAeA,SAAgB,cAAc,EAAE,WAAW,GAAG,SAA6B;CACzE,OACE,oBAACC,YAAD;EACE,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,sBAAsB,GAAG,SAAS;CACjD,CAAA;AAEL;AAiCA,SAAgB,iBAAiB,EAC/B,UACA,MACA,WACA,eAAe,KACS;CACxB,OACE,oBAAC,SAAD;EAAS,OAAO;EACd,UAAA,qBAACC,QAAD;GACE,MAAK;GACL,aAAU;GACV,WAAW,GAAG,yBAAyB,GAAG,SAAS;GAHrD,UAAA,CAKG,UACA,IACS;;CACL,CAAA;AAEb;AAgBA,SAAgB,eAAe,EAAE,WAAW,GAAG,SAA8B;CAC3E,OACE,oBAACC,iBAAD;EACE,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,uBAAuB,GAAG,SAAS;CAClD,CAAA;AAEL"}
1
+ {"version":3,"file":"accordion.js","names":["AriaDisclosureGroup","AriaDisclosure","AriaButton","AriaDisclosurePanel"],"sources":["../../src/accordion/accordion.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { forwardRef, type ReactNode } from \"react\";\nimport {\n Button as AriaButton,\n Disclosure as AriaDisclosure,\n DisclosureGroup as AriaDisclosureGroup,\n DisclosurePanel as AriaDisclosurePanel,\n Heading,\n type DisclosureGroupProps as AriaDisclosureGroupProps,\n type DisclosurePanelProps as AriaDisclosurePanelProps,\n type DisclosureProps as AriaDisclosureProps,\n} from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\n\n/**\n * Accordion, ported from the legacy shadcn/Radix component onto React Aria's\n * Disclosure primitives.\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 * State comes from React Aria data attributes (`data-expanded`,\n * `data-focus-visible`, `data-disabled`), never from pseudo-classes.\n *\n * ONE BEHAVIOUR IS DELIBERATELY DROPPED. Radix moved focus between headers\n * with Home/End and the arrow keys; React Aria's useDisclosure has no key\n * handling, so each trigger is a button in the tab order and arrows do\n * nothing. The ARIA APG lists arrow navigation for accordions as optional,\n * and hand-rolling roving focus would mean owning focus management that the\n * headless base deliberately does not. The TabReachesEveryTrigger story pins\n * this so it stays a decision rather than becoming a surprise.\n */\nexport const accordionVariants = cva(\"w-full\");\n\nexport const accordionItemVariants = cva([\n // The divider between items. border-b-0 on the last child rather than\n // border-t on siblings: it survives a single-item accordion, which the\n // sibling form does not.\n \"border-b-(length:--border-width) border-b-border last:border-b-0\",\n // The chevron rotation is authored HERE, not on the trigger: React Aria\n // puts data-expanded on the Disclosure, and CSS cannot look upward from\n // the button to its expanded ancestor.\n \"data-[expanded]:[&_[data-slot=accordion-trigger]_svg]:rotate-180\",\n]);\n\nexport const accordionTriggerVariants = cva([\n \"flex w-full items-center justify-between gap-(--density-control-gap)\",\n // text-start, never text-left. Logical properties are what make RTL work\n // without a second stylesheet (ARCHITECTURE section 5 rule 4).\n \"text-start text-sm leading-(--leading-normal) font-sans font-medium text-foreground\",\n \"rounded-md py-(--density-control-pad-md)\",\n \"cursor-pointer select-none\",\n\n // The reset in @seekalfred/styles is opt-in, so a consumer may load none.\n // A bare <button> then keeps the user-agent border, ButtonFace grey and\n // Arial -- the same defence the Button base carries, for the same reason.\n \"border-(length:--border-width) border-transparent bg-transparent\",\n\n // A real outline, not a ring. The legacy component focused with\n // ring-[3px], a box-shadow, which forced-colors discards entirely -- and\n // axe has no focus-indicator rule, so nothing would have caught it.\n // outline-solid is load-bearing: outline-none sets --tw-outline-style:none\n // and the width utility resolves its style from that same variable, so\n // without it the ring gets a width and a colour and paints nothing.\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 \"motion-safe:transition-colors motion-safe:duration-(--duration-fast) motion-safe:ease-standard\",\n \"data-[hovered]:underline\",\n \"data-[disabled]:cursor-not-allowed data-[disabled]:opacity-(--opacity-disabled)\",\n\n // The chevron. Sized from the icon scale unless the caller sized it, and\n // never a press target of its own.\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0\",\n \"[&_svg:not([class*='size-'])]:size-(--icon-sm)\",\n \"[&_svg]:text-muted-foreground\",\n // Rotation is the whole motion story for this component -- see the panel\n // note below for why the panel itself does not animate.\n \"[&_svg]:motion-safe:transition-transform [&_svg]:motion-safe:duration-(--duration-snappy) [&_svg]:motion-safe:ease-snappy\",\n // The icon takes its full colour once its panel is open.\n \"[&_svg]:motion-safe:transition-[transform,color]\",\n]);\n\nexport const accordionPanelVariants = cva([\n // font-sans is not decoration. The panel is a plain <div> inheriting from\n // the consumer's body, and the library deliberately does not style the\n // page -- so without this the copy renders in the browser's default serif\n // while the trigger above it renders in the brand face. Caught by eye in\n // the browser, not by any assertion, which is why NoUaLeak now covers the\n // panel too.\n \"pb-(--density-control-pad-md) font-sans text-sm leading-(--leading-normal) text-foreground\",\n // Opens and closes in place (see the accordion-panel utility): the collapsed\n // panel is kept as a box of no height, out of the accessibility tree.\n \"accordion-panel\",\n]);\n\n/**\n * A grouping of collapsible sections.\n *\n * Expansion is KEYED, which is the sharpest break from the legacy component:\n * `type=\"single\" | \"multiple\"` becomes {@link AccordionProps.allowsMultipleExpanded},\n * and `value` / `defaultValue` strings become `expandedKeys` /\n * `defaultExpandedKeys` holding item ids. The change handler receives a\n * `Set`, not a string.\n *\n * The legacy `collapsible` prop has no equivalent and needs none: React\n * Aria's single-expand mode always allows collapsing the open item.\n *\n * @example\n * <Accordion defaultExpandedKeys={[\"billing\"]}>\n * <AccordionItem id=\"billing\">\n * <AccordionTrigger icon={<ChevronDown aria-hidden />}>Billing</AccordionTrigger>\n * <AccordionPanel>Invoices and payment methods.</AccordionPanel>\n * </AccordionItem>\n * </Accordion>\n */\nexport interface AccordionProps\n extends\n Omit<AriaDisclosureGroupProps, \"className\" | \"style\">,\n VariantProps<typeof accordionVariants> {\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nexport const Accordion = forwardRef<HTMLDivElement, AccordionProps>(function Accordion(\n { className, ...props },\n ref,\n) {\n return (\n <AriaDisclosureGroup\n ref={ref}\n {...props}\n data-slot=\"accordion\"\n className={cn(accordionVariants(), className)}\n />\n );\n});\n\n/**\n * One collapsible section.\n *\n * `id` replaces the legacy `value`, and is what `expandedKeys` refers to.\n */\nexport interface AccordionItemProps\n extends\n Omit<AriaDisclosureProps, \"className\" | \"style\">,\n VariantProps<typeof accordionItemVariants> {\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nexport const AccordionItem = forwardRef<HTMLDivElement, AccordionItemProps>(function AccordionItem(\n { className, ...props },\n ref,\n) {\n return (\n <AriaDisclosure\n ref={ref}\n {...props}\n data-slot=\"accordion-item\"\n className={cn(accordionItemVariants(), className)}\n />\n );\n});\n\n/**\n * The header button that expands its item.\n *\n * Rendered inside a {@link Heading} because screen-reader users navigate an\n * accordion by heading; that wrapper is what makes it usable rather than\n * merely operable.\n */\nexport interface AccordionTriggerProps extends VariantProps<typeof accordionTriggerVariants> {\n /** The header label. */\n children?: ReactNode;\n /**\n * The expand/collapse glyph, rendered after the label.\n *\n * Required, and supplied by the caller: this library ships no icon set, so\n * it stays icon-library agnostic. Mark the glyph `aria-hidden` -- the label\n * is the accessible name, and a second name source makes the trigger\n * announce twice. It is sized at `--icon-sm` unless it carries a `size-*`\n * class of its own, and rotates on expand.\n */\n icon: ReactNode;\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n /**\n * Heading level for the wrapper. Accordions appear at different depths, and\n * a fixed level would break the document outline wherever it is wrong.\n *\n * @default 3\n */\n headingLevel?: number;\n}\n\nexport const AccordionTrigger = forwardRef<HTMLButtonElement, AccordionTriggerProps>(\n function AccordionTrigger({ children, icon, className, headingLevel = 3 }, ref) {\n return (\n <Heading level={headingLevel}>\n <AriaButton\n // The ref goes on the button, not the Heading wrapping it: Heading has\n // no interactive behaviour, while the button is what expands/collapses\n // and carries the press/focus data attributes.\n ref={ref}\n slot=\"trigger\"\n data-slot=\"accordion-trigger\"\n className={cn(accordionTriggerVariants(), className)}\n >\n {children}\n {icon}\n </AriaButton>\n </Heading>\n );\n },\n);\n\n/**\n * The content revealed when its item expands.\n *\n * Named Panel rather than the legacy Content, matching React Aria and the\n * ARIA pattern.\n */\nexport interface AccordionPanelProps\n extends\n Omit<AriaDisclosurePanelProps, \"className\" | \"style\">,\n VariantProps<typeof accordionPanelVariants> {\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nexport const AccordionPanel = forwardRef<HTMLDivElement, AccordionPanelProps>(\n function AccordionPanel({ className, children, ...props }, ref) {\n return (\n <AriaDisclosurePanel\n ref={ref}\n {...props}\n data-slot=\"accordion-panel\"\n className={cn(accordionPanelVariants(), className)}\n >\n {/* A wrapper, so the answer can settle down into place while the panel opens. */}\n <div data-slot=\"accordion-panel-content\" className=\"accordion-panel-content\">\n {children}\n </div>\n </AriaDisclosurePanel>\n );\n },\n);\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCA,MAAa,oBAAoB,IAAI,QAAQ;AAE7C,MAAa,wBAAwB,IAAI,CAIvC,oEAIA,kEACF,CAAC;AAED,MAAa,2BAA2B,IAAI;CAC1C;CAGA;CACA;CACA;CAKA;CAQA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CAIA;CACA;CACA;CAGA;CAEA;AACF,CAAC;AAED,MAAa,yBAAyB,IAAI,CAOxC,8FAGA,iBACF,CAAC;AA8BD,MAAa,YAAY,WAA2C,SAAS,UAC3E,EAAE,WAAW,GAAG,SAChB,KACA;CACA,OACE,oBAACA,iBAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,kBAAkB,GAAG,SAAS;CAC7C,CAAA;AAEL,CAAC;AAeD,MAAa,gBAAgB,WAA+C,SAAS,cACnF,EAAE,WAAW,GAAG,SAChB,KACA;CACA,OACE,oBAACC,YAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,sBAAsB,GAAG,SAAS;CACjD,CAAA;AAEL,CAAC;AAiCD,MAAa,mBAAmB,WAC9B,SAAS,iBAAiB,EAAE,UAAU,MAAM,WAAW,eAAe,KAAK,KAAK;CAC9E,OACE,oBAAC,SAAD;EAAS,OAAO;EACd,UAAA,qBAACC,QAAD;GAIO;GACL,MAAK;GACL,aAAU;GACV,WAAW,GAAG,yBAAyB,GAAG,SAAS;GAPrD,UAAA,CASG,UACA,IACS;;CACL,CAAA;AAEb,CACF;AAgBA,MAAa,iBAAiB,WAC5B,SAAS,eAAe,EAAE,WAAW,UAAU,GAAG,SAAS,KAAK;CAC9D,OACE,oBAACC,iBAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,uBAAuB,GAAG,SAAS;EAGjD,UAAA,oBAAC,OAAD;GAAK,aAAU;GAA0B,WAAU;GAChD;EACE,CAAA;CACc,CAAA;AAEzB,CACF"}
@@ -0,0 +1,50 @@
1
+ import { buttonVariants } from "../button/button.js";
2
+ import { VariantProps } from "class-variance-authority";
3
+ import { ButtonProps } from "react-aria-components";
4
+ //#region src/action-button/action-button.d.ts
5
+ /**
6
+ * Props.
7
+ *
8
+ * Deliberately NOT Button's props: an action button owns its own content (the
9
+ * morphing label and the arrow, spinner or tick beside it), so `children` is the
10
+ * resting label as a plain string rather than arbitrary content, and `onAction`
11
+ * replaces `onPress` because it is awaited. It does take Button's `variant` and
12
+ * `size`, so it looks like the Button next to it.
13
+ */
14
+ export interface ActionButtonProps extends Omit<ButtonProps, "className" | "style" | "onPress" | "children" | "isPending">, VariantProps<typeof buttonVariants> {
15
+ /** The label at rest. A plain string: it is split into letters to morph. */
16
+ children: string;
17
+ /** The label once the action succeeds. */
18
+ successLabel?: string;
19
+ /** The label while the action runs. */
20
+ pendingLabel?: string;
21
+ /**
22
+ * What the button does. If it returns a promise the button stays pending
23
+ * until it settles; a rejection returns the button to rest and goes to
24
+ * `onActionError`. A press while pending is ignored.
25
+ */
26
+ onAction: () => void | Promise<void>;
27
+ /** How long the success state shows before the button returns to rest, in ms. `0` keeps it. */
28
+ resetAfterMs?: number;
29
+ /** Called when `onAction` throws or rejects. Without it the error is dropped, because the button has already returned to rest. */
30
+ onActionError?: (error: unknown) => void;
31
+ /** Extra classes. Merged last, so they win conflicts with ours. */
32
+ className?: string;
33
+ }
34
+ /**
35
+ * A button that runs an action and shows it through: its label morphs from the
36
+ * resting text to a pending one and then a success one, the arrow becomes a
37
+ * spinner and then a tick that draws itself, and the button's width follows the
38
+ * text on a spring.
39
+ *
40
+ * Use it for one async commit ("Save", "Send"). It stays focusable while
41
+ * pending (`aria-disabled`, not disabled), so a keyboard user keeps focus
42
+ * through the whole action, and a live region announces the pending and success
43
+ * labels. The accessible name is always the resting label.
44
+ *
45
+ * @example
46
+ * <ActionButton onAction={save} pendingLabel="Saving" successLabel="Saved">Save changes</ActionButton>
47
+ */
48
+ export declare const ActionButton: import("react").ForwardRefExoticComponent<ActionButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
49
+ //#endregion
50
+ //# sourceMappingURL=action-button.d.ts.map
@@ -0,0 +1,159 @@
1
+ "use client";
2
+ import { cn } from "../lib/cn.js";
3
+ import { mergeRefs } from "../lib/merge-refs.js";
4
+ import { anchorsLayer, pressScaleFor, useAriaBusy, useExitBackstop, useLeavingItems } from "../lib/motion.js";
5
+ import { buttonVariants } from "../button/button.js";
6
+ import { MorphText } from "../lib/morph-text.js";
7
+ import "class-variance-authority";
8
+ import { forwardRef, useCallback, useEffect, useRef, useState } from "react";
9
+ import { Button } from "react-aria-components";
10
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
11
+ //#region src/action-button/action-button.tsx
12
+ const ICON = "size-(--icon-sm)";
13
+ /** The tick draws itself from its short stroke, the way a hand would write it. */
14
+ function DrawnCheck() {
15
+ return /* @__PURE__ */ jsx("svg", {
16
+ className: ICON,
17
+ viewBox: "0 0 24 24",
18
+ fill: "none",
19
+ stroke: "currentColor",
20
+ strokeWidth: 2,
21
+ strokeLinecap: "round",
22
+ strokeLinejoin: "round",
23
+ "aria-hidden": "true",
24
+ children: /* @__PURE__ */ jsx("path", {
25
+ d: "M4 12l5 5L20 6",
26
+ pathLength: 1,
27
+ className: "[stroke-dasharray:1] motion-safe:animate-check-draw"
28
+ })
29
+ });
30
+ }
31
+ function Arrow() {
32
+ return /* @__PURE__ */ jsx("svg", {
33
+ className: ICON,
34
+ viewBox: "0 0 24 24",
35
+ fill: "none",
36
+ stroke: "currentColor",
37
+ strokeWidth: 2,
38
+ strokeLinecap: "round",
39
+ strokeLinejoin: "round",
40
+ "aria-hidden": "true",
41
+ children: /* @__PURE__ */ jsx("path", { d: "M5 12h14M12 5l7 7-7 7" })
42
+ });
43
+ }
44
+ function Spinner() {
45
+ return /* @__PURE__ */ jsx("span", { className: "size-4 rounded-circle border-(length:--border-width-spinner) border-current border-r-transparent motion-safe:animate-spin-fast" });
46
+ }
47
+ function PhaseIcon({ state }) {
48
+ if (state === "pending") return /* @__PURE__ */ jsx(Spinner, {});
49
+ if (state === "success") return /* @__PURE__ */ jsx(DrawnCheck, {});
50
+ return /* @__PURE__ */ jsx(Arrow, {});
51
+ }
52
+ /** Classes for one phase of the icon slot. The arrow moves; the spinner and tick scale. */
53
+ function phaseClasses(state, leaving, animated) {
54
+ const arrow = state === "idle";
55
+ if (leaving) return arrow ? "motion-safe:animate-arrow-out motion-reduce:animate-fade-out" : "motion-safe:animate-glyph-out motion-reduce:animate-fade-out";
56
+ if (!animated) return "";
57
+ return arrow ? "motion-safe:animate-arrow-in motion-reduce:animate-fade-in" : "motion-safe:animate-glyph-in motion-reduce:animate-fade-in";
58
+ }
59
+ function IconSlot({ state }) {
60
+ const phaseRef = useRef(null);
61
+ const { leaving, changed, remove } = useLeavingItems(state, state, () => phaseRef.current);
62
+ return /* @__PURE__ */ jsxs("span", {
63
+ className: cn("grid shrink-0 place-items-center", ICON),
64
+ children: [leaving.map((item) => /* @__PURE__ */ jsx(LeavingIcon, {
65
+ id: item.id,
66
+ state: item.value,
67
+ onDone: remove
68
+ }, item.id)), /* @__PURE__ */ jsx("span", {
69
+ ref: phaseRef,
70
+ className: cn(`col-start-1 row-start-1 grid place-items-center ${ICON} ${phaseClasses(state, false, changed)}`),
71
+ children: /* @__PURE__ */ jsx(PhaseIcon, { state })
72
+ }, state)]
73
+ });
74
+ }
75
+ function LeavingIcon({ id, state, onDone }) {
76
+ const remove = useCallback(() => onDone(id), [onDone, id]);
77
+ useExitBackstop(true, remove);
78
+ return /* @__PURE__ */ jsx("span", {
79
+ "aria-hidden": "true",
80
+ onAnimationEnd: (event) => {
81
+ if (event.target === event.currentTarget) remove();
82
+ },
83
+ className: cn(`col-start-1 row-start-1 grid place-items-center ${ICON} ${phaseClasses(state, true, true)}`),
84
+ children: /* @__PURE__ */ jsx(PhaseIcon, { state })
85
+ });
86
+ }
87
+ /**
88
+ * A button that runs an action and shows it through: its label morphs from the
89
+ * resting text to a pending one and then a success one, the arrow becomes a
90
+ * spinner and then a tick that draws itself, and the button's width follows the
91
+ * text on a spring.
92
+ *
93
+ * Use it for one async commit ("Save", "Send"). It stays focusable while
94
+ * pending (`aria-disabled`, not disabled), so a keyboard user keeps focus
95
+ * through the whole action, and a live region announces the pending and success
96
+ * labels. The accessible name is always the resting label.
97
+ *
98
+ * @example
99
+ * <ActionButton onAction={save} pendingLabel="Saving" successLabel="Saved">Save changes</ActionButton>
100
+ */
101
+ const ActionButton = forwardRef(function ActionButton({ children: label, successLabel = "Saved", pendingLabel = "Saving", onAction, resetAfterMs = 2400, onActionError, className, variant, size, onPressStart, ...props }, ref) {
102
+ const [state, setState] = useState("idle");
103
+ const resetTimer = useRef(null);
104
+ const busy = useAriaBusy(state === "pending");
105
+ useEffect(() => () => {
106
+ if (resetTimer.current) clearTimeout(resetTimer.current);
107
+ }, []);
108
+ async function run() {
109
+ if (state === "pending") return;
110
+ if (resetTimer.current) clearTimeout(resetTimer.current);
111
+ setState("pending");
112
+ try {
113
+ await onAction();
114
+ setState("success");
115
+ if (resetAfterMs > 0) resetTimer.current = setTimeout(() => setState("idle"), resetAfterMs);
116
+ } catch (error) {
117
+ setState("idle");
118
+ onActionError?.(error);
119
+ }
120
+ }
121
+ const pressStart = (event) => {
122
+ const target = event.target;
123
+ if (state === "pending" || anchorsLayer(props)) target.style.removeProperty("--press-scale");
124
+ else target.style.setProperty("--press-scale", pressScaleFor(target.offsetWidth));
125
+ onPressStart?.(event);
126
+ };
127
+ const text = state === "pending" ? pendingLabel : state === "success" ? successLabel : label;
128
+ const status = state === "pending" ? pendingLabel : state === "success" ? successLabel : "";
129
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs(Button, {
130
+ ...props,
131
+ ref: mergeRefs(ref, busy),
132
+ "aria-disabled": state === "pending" ? true : props["aria-disabled"],
133
+ "data-pending": state === "pending" || void 0,
134
+ onPress: () => void run(),
135
+ onPressStart: pressStart,
136
+ "data-slot": "button",
137
+ "data-status": state,
138
+ className: cn(`${buttonVariants({
139
+ variant,
140
+ size
141
+ })} relative overflow-hidden`, className),
142
+ children: [/* @__PURE__ */ jsxs("span", {
143
+ className: "inline-flex min-h-5 items-center justify-center gap-2 whitespace-nowrap",
144
+ "aria-hidden": "true",
145
+ children: [/* @__PURE__ */ jsx(MorphText, { text }), /* @__PURE__ */ jsx(IconSlot, { state })]
146
+ }), /* @__PURE__ */ jsx("span", {
147
+ className: "sr-only",
148
+ children: label
149
+ })]
150
+ }), /* @__PURE__ */ jsx("span", {
151
+ className: "sr-only",
152
+ role: "status",
153
+ children: status
154
+ })] });
155
+ });
156
+ //#endregion
157
+ export { ActionButton };
158
+
159
+ //# sourceMappingURL=action-button.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action-button.js","names":["AriaButton"],"sources":["../../src/action-button/action-button.tsx"],"sourcesContent":["\"use client\";\n\nimport { type VariantProps } from \"class-variance-authority\";\nimport { forwardRef, useCallback, useEffect, useRef, useState, type RefObject } from \"react\";\nimport {\n Button as AriaButton,\n type ButtonProps as AriaButtonProps,\n type PressEvent,\n} from \"react-aria-components\";\n\nimport { buttonVariants } from \"../button/button.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { mergeRefs } from \"../lib/merge-refs.js\";\nimport { MorphText } from \"../lib/morph-text.js\";\nimport {\n anchorsLayer,\n pressScaleFor,\n useAriaBusy,\n useExitBackstop,\n useLeavingItems,\n} from \"../lib/motion.js\";\n\ntype ActionState = \"idle\" | \"pending\" | \"success\";\n\n/**\n * Props.\n *\n * Deliberately NOT Button's props: an action button owns its own content (the\n * morphing label and the arrow, spinner or tick beside it), so `children` is the\n * resting label as a plain string rather than arbitrary content, and `onAction`\n * replaces `onPress` because it is awaited. It does take Button's `variant` and\n * `size`, so it looks like the Button next to it.\n */\nexport interface ActionButtonProps\n extends\n Omit<AriaButtonProps, \"className\" | \"style\" | \"onPress\" | \"children\" | \"isPending\">,\n VariantProps<typeof buttonVariants> {\n /** The label at rest. A plain string: it is split into letters to morph. */\n children: string;\n /** The label once the action succeeds. */\n successLabel?: string;\n /** The label while the action runs. */\n pendingLabel?: string;\n /**\n * What the button does. If it returns a promise the button stays pending\n * until it settles; a rejection returns the button to rest and goes to\n * `onActionError`. A press while pending is ignored.\n */\n onAction: () => void | Promise<void>;\n /** How long the success state shows before the button returns to rest, in ms. `0` keeps it. */\n resetAfterMs?: number;\n /** Called when `onAction` throws or rejects. Without it the error is dropped, because the button has already returned to rest. */\n onActionError?: (error: unknown) => void;\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nconst ICON = \"size-(--icon-sm)\";\n\n/** The tick draws itself from its short stroke, the way a hand would write it. */\nfunction DrawnCheck() {\n return (\n <svg\n className={ICON}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M4 12l5 5L20 6\"\n pathLength={1}\n className=\"[stroke-dasharray:1] motion-safe:animate-check-draw\"\n />\n </svg>\n );\n}\n\nfunction Arrow() {\n return (\n <svg\n className={ICON}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M5 12h14M12 5l7 7-7 7\" />\n </svg>\n );\n}\n\nfunction Spinner() {\n return (\n <span className=\"size-4 rounded-circle border-(length:--border-width-spinner) border-current border-r-transparent motion-safe:animate-spin-fast\" />\n );\n}\n\nfunction PhaseIcon({ state }: { state: ActionState }) {\n if (state === \"pending\") return <Spinner />;\n if (state === \"success\") return <DrawnCheck />;\n return <Arrow />;\n}\n\n/** Classes for one phase of the icon slot. The arrow moves; the spinner and tick scale. */\nfunction phaseClasses(state: ActionState, leaving: boolean, animated: boolean) {\n const arrow = state === \"idle\";\n if (leaving) {\n return arrow\n ? \"motion-safe:animate-arrow-out motion-reduce:animate-fade-out\"\n : \"motion-safe:animate-glyph-out motion-reduce:animate-fade-out\";\n }\n if (!animated) return \"\";\n return arrow\n ? \"motion-safe:animate-arrow-in motion-reduce:animate-fade-in\"\n : \"motion-safe:animate-glyph-in motion-reduce:animate-fade-in\";\n}\n\nfunction IconSlot({ state }: { state: ActionState }) {\n const phaseRef = useRef<HTMLSpanElement>(null);\n // The phases share one grid cell, so a leaving one needs no rect: it simply stays under the new one.\n const { leaving, changed, remove } = useLeavingItems<ActionState>(\n state,\n state,\n () => phaseRef.current,\n );\n return (\n <span className={cn(\"grid shrink-0 place-items-center\", ICON)}>\n {leaving.map((item) => (\n <LeavingIcon key={item.id} id={item.id} state={item.value} onDone={remove} />\n ))}\n <span\n key={state}\n ref={phaseRef}\n className={cn(\n `col-start-1 row-start-1 grid place-items-center ${ICON} ${phaseClasses(state, false, changed)}`,\n )}\n >\n <PhaseIcon state={state} />\n </span>\n </span>\n );\n}\n\nfunction LeavingIcon({\n id,\n state,\n onDone,\n}: {\n id: number;\n state: ActionState;\n onDone: (id: number) => void;\n}) {\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 className={cn(\n `col-start-1 row-start-1 grid place-items-center ${ICON} ${phaseClasses(state, true, true)}`,\n )}\n >\n <PhaseIcon state={state} />\n </span>\n );\n}\n\n/**\n * A button that runs an action and shows it through: its label morphs from the\n * resting text to a pending one and then a success one, the arrow becomes a\n * spinner and then a tick that draws itself, and the button's width follows the\n * text on a spring.\n *\n * Use it for one async commit (\"Save\", \"Send\"). It stays focusable while\n * pending (`aria-disabled`, not disabled), so a keyboard user keeps focus\n * through the whole action, and a live region announces the pending and success\n * labels. The accessible name is always the resting label.\n *\n * @example\n * <ActionButton onAction={save} pendingLabel=\"Saving\" successLabel=\"Saved\">Save changes</ActionButton>\n */\nexport const ActionButton = forwardRef<HTMLButtonElement, ActionButtonProps>(function ActionButton(\n {\n children: label,\n successLabel = \"Saved\",\n pendingLabel = \"Saving\",\n onAction,\n resetAfterMs = 2400,\n onActionError,\n className,\n variant,\n size,\n onPressStart,\n ...props\n },\n ref,\n) {\n const [state, setState] = useState<ActionState>(\"idle\");\n const resetTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n const busy = useAriaBusy(state === \"pending\");\n\n useEffect(\n () => () => {\n if (resetTimer.current) clearTimeout(resetTimer.current);\n },\n [],\n );\n\n async function run() {\n if (state === \"pending\") return;\n if (resetTimer.current) clearTimeout(resetTimer.current);\n setState(\"pending\");\n try {\n await onAction();\n setState(\"success\");\n if (resetAfterMs > 0) resetTimer.current = setTimeout(() => setState(\"idle\"), resetAfterMs);\n } catch (error) {\n setState(\"idle\");\n onActionError?.(error);\n }\n }\n\n const pressStart = (event: PressEvent) => {\n const target = event.target as HTMLElement;\n // A pending button does not scale: drop the depth a previous press left behind.\n if (state === \"pending\" || anchorsLayer(props)) target.style.removeProperty(\"--press-scale\");\n else target.style.setProperty(\"--press-scale\", pressScaleFor(target.offsetWidth));\n onPressStart?.(event);\n };\n\n const text = state === \"pending\" ? pendingLabel : state === \"success\" ? successLabel : label;\n const status = state === \"pending\" ? pendingLabel : state === \"success\" ? successLabel : \"\";\n\n return (\n <>\n <AriaButton\n {...props}\n ref={mergeRefs(ref, busy as RefObject<HTMLButtonElement | null>)}\n // Not React Aria's isPending: it announces through a transient role=\"img\" node\n // whose label id does not resolve, which axe rejects, and this button has its\n // own live region. aria-disabled and aria-busy keep it focusable and announced.\n aria-disabled={state === \"pending\" ? true : props[\"aria-disabled\"]}\n data-pending={state === \"pending\" || undefined}\n onPress={() => void run()}\n onPressStart={pressStart}\n data-slot=\"button\"\n data-status={state}\n className={cn(`${buttonVariants({ variant, size })} relative overflow-hidden`, className)}\n >\n <span\n className=\"inline-flex min-h-5 items-center justify-center gap-2 whitespace-nowrap\"\n aria-hidden=\"true\"\n >\n <MorphText text={text} />\n <IconSlot state={state} />\n </span>\n <span className=\"sr-only\">{label}</span>\n </AriaButton>\n {/* Beside the button, not inside it: inside, its text would join the button's accessible name. */}\n <span className=\"sr-only\" role=\"status\">\n {status}\n </span>\n </>\n );\n});\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { ActionButton, ActionButtonProps } from "./action-button.js";
2
+ export { ActionButton, type ActionButtonProps };
@@ -0,0 +1,2 @@
1
+ import { ActionButton } from "./action-button.js";
2
+ export { ActionButton };
@@ -24,7 +24,7 @@ import { ComponentPropsWithoutRef, ReactNode } from "react";
24
24
  * problems rather than porting them.
25
25
  */
26
26
  export declare const alertVariants: (props?: ({
27
- variant?: "destructive" | "default" | null | undefined;
27
+ variant?: "default" | "destructive" | null | undefined;
28
28
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
29
29
  /**
30
30
  * An inline status message.
@@ -62,7 +62,21 @@ export interface AlertProps extends Omit<ComponentPropsWithoutRef<"div">, "title
62
62
  * @default 3
63
63
  */
64
64
  headingLevel?: 2 | 3 | 4 | 5 | 6;
65
+ /**
66
+ * Whether the alert is shown. Controlled: hiding it collapses its height and
67
+ * fades it out, and showing it again grows it back. An alert that is open on
68
+ * first render does not animate. Leave it out for an alert that is always there.
69
+ */
70
+ open?: boolean;
71
+ /**
72
+ * Adds a Dismiss button. Pressed, a controlled alert (`open` given) only calls
73
+ * this and waits to be told `open={false}`; an uncontrolled alert (no `open`)
74
+ * collapses first and then calls it.
75
+ */
76
+ onDismiss?: () => void;
77
+ /** The Dismiss button's accessible name. @default "Dismiss" */
78
+ dismissLabel?: string;
65
79
  }
66
- export declare function Alert({ className, variant, icon, title, headingLevel, children, ...props }: AlertProps): import("react").JSX.Element;
80
+ export declare const Alert: import("react").ForwardRefExoticComponent<AlertProps & import("react").RefAttributes<HTMLDivElement>>;
67
81
  //#endregion
68
82
  //# sourceMappingURL=alert.d.ts.map
@@ -1,7 +1,12 @@
1
1
  "use client";
2
2
  import { cn } from "../lib/cn.js";
3
+ import { prefersReducedMotion, useExitBackstop, useIsomorphicLayoutEffect } from "../lib/motion.js";
4
+ import { SwapText } from "../lib/swap-text.js";
5
+ import { useHeightFollow } from "../lib/height-follow.js";
3
6
  import { cva } from "class-variance-authority";
4
- import { jsx, jsxs } from "react/jsx-runtime";
7
+ import { forwardRef, isValidElement, useCallback, useEffect, useRef, useState } from "react";
8
+ import { Button } from "react-aria-components";
9
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
5
10
  //#region src/alert/alert.tsx
6
11
  /**
7
12
  * Alert, ported from the legacy shadcn component.
@@ -36,31 +41,124 @@ const alertVariants = cva([
36
41
  } },
37
42
  defaultVariants: { variant: "default" }
38
43
  });
39
- function Alert({ className, variant, icon, title, headingLevel = 3, children, ...props }) {
44
+ /** A string title or description swaps as a whole when its text changes. */
45
+ function Swappable({ value, children }) {
46
+ if (typeof value !== "string" && typeof value !== "number") return /* @__PURE__ */ jsx(Fragment$1, { children });
47
+ return /* @__PURE__ */ jsx(SwapText, {
48
+ swapKey: String(value),
49
+ className: "block",
50
+ children
51
+ });
52
+ }
53
+ /** The name an icon swaps under: a different icon component crossfades; the same one re-rendering stays still. */
54
+ function iconKey(icon) {
55
+ if (!isValidElement(icon)) return "icon";
56
+ const type = icon.type;
57
+ return typeof type === "string" ? type : type.displayName ?? type.name ?? "icon";
58
+ }
59
+ function DismissGlyph() {
60
+ return /* @__PURE__ */ jsx("svg", {
61
+ viewBox: "0 0 16 16",
62
+ fill: "none",
63
+ stroke: "currentColor",
64
+ strokeWidth: "1.75",
65
+ strokeLinecap: "round",
66
+ "aria-hidden": "true",
67
+ className: "size-4",
68
+ children: /* @__PURE__ */ jsx("path", { d: "M4 4l8 8M12 4l-8 8" })
69
+ });
70
+ }
71
+ const Alert = forwardRef(function Alert({ className, variant, icon, title, headingLevel = 3, children, open, onDismiss, dismissLabel = "Dismiss", ...props }, ref) {
40
72
  const Title = `h${headingLevel}`;
41
- return /* @__PURE__ */ jsxs("div", {
73
+ const frame = useRef(null);
74
+ const content = useRef(null);
75
+ useHeightFollow(frame, content, `${typeof title === "string" ? title : ""}\n${children === void 0 ? "" : "details"}\n${typeof children === "string" || typeof children === "number" ? String(children) : ""}`);
76
+ const managed = open !== void 0 || onDismiss !== void 0;
77
+ const [dismissed, setDismissed] = useState(false);
78
+ const wanted = open ?? !dismissed;
79
+ const [rendered, setRendered] = useState(wanted);
80
+ const [shown, setShown] = useState(wanted);
81
+ if (wanted && !rendered) setRendered(true);
82
+ if (!wanted && shown) setShown(false);
83
+ useEffect(() => {
84
+ if (wanted && rendered && !shown) {
85
+ const frameId = requestAnimationFrame(() => setShown(true));
86
+ return () => cancelAnimationFrame(frameId);
87
+ }
88
+ }, [
89
+ wanted,
90
+ rendered,
91
+ shown
92
+ ]);
93
+ const leaving = rendered && !wanted;
94
+ const finish = useCallback(() => {
95
+ setRendered(false);
96
+ if (open === void 0) onDismiss?.();
97
+ }, [onDismiss, open]);
98
+ useExitBackstop(leaving, finish, 900);
99
+ useIsomorphicLayoutEffect(() => {
100
+ if (leaving && prefersReducedMotion()) finish();
101
+ }, [leaving, finish]);
102
+ if (managed && !rendered) return null;
103
+ const alert = /* @__PURE__ */ jsxs("div", {
104
+ ref,
42
105
  ...props,
43
106
  role: "alert",
44
107
  "data-slot": "alert",
45
108
  className: cn(alertVariants({ variant }), className),
46
- children: [icon === void 0 ? null : /* @__PURE__ */ jsx("span", {
47
- "data-slot": "alert-icon",
48
- className: "shrink-0 [&>svg]:pointer-events-none [&>svg:not([class*='size-'])]:size-(--icon-sm)",
49
- children: icon
50
- }), /* @__PURE__ */ jsxs("div", {
51
- "data-slot": "alert-body",
52
- className: "min-w-0 flex-1",
53
- children: [title === void 0 ? null : /* @__PURE__ */ jsx(Title, {
54
- "data-slot": "alert-title",
55
- className: "mb-1 text-sm leading-(--leading-tight) font-medium",
56
- children: title
57
- }), children === void 0 ? null : /* @__PURE__ */ jsx("div", {
58
- "data-slot": "alert-description",
59
- children
60
- })]
61
- })]
109
+ children: [
110
+ icon === void 0 ? null : /* @__PURE__ */ jsx("span", {
111
+ "data-slot": "alert-icon",
112
+ className: "grid shrink-0 [&>svg]:pointer-events-none [&>svg:not([class*='size-'])]:size-(--icon-sm)",
113
+ children: /* @__PURE__ */ jsx(SwapText, {
114
+ swapKey: iconKey(icon),
115
+ motion: "glyph",
116
+ className: "grid place-items-center",
117
+ children: icon
118
+ })
119
+ }),
120
+ /* @__PURE__ */ jsx("div", {
121
+ ref: frame,
122
+ "data-slot": "alert-body",
123
+ className: "height-follow min-w-0 flex-1",
124
+ children: /* @__PURE__ */ jsxs("div", {
125
+ ref: content,
126
+ children: [title === void 0 ? null : /* @__PURE__ */ jsx(Title, {
127
+ "data-slot": "alert-title",
128
+ className: "mb-1 text-sm leading-(--leading-tight) font-medium",
129
+ children: /* @__PURE__ */ jsx(Swappable, {
130
+ value: title,
131
+ children: title
132
+ })
133
+ }), children === void 0 ? null : /* @__PURE__ */ jsx("div", {
134
+ "data-slot": "alert-description",
135
+ children: /* @__PURE__ */ jsx(Swappable, {
136
+ value: children,
137
+ children
138
+ })
139
+ })]
140
+ })
141
+ }),
142
+ onDismiss ? /* @__PURE__ */ jsx(Button, {
143
+ "data-slot": "alert-dismiss",
144
+ "aria-label": typeof title === "string" ? `${dismissLabel}: ${title}` : dismissLabel,
145
+ onPress: () => open === void 0 ? setDismissed(true) : onDismiss(),
146
+ className: "calendar-nav-motion -my-1 -me-1.5 grid size-7 shrink-0 cursor-pointer place-items-center rounded-full border-0 bg-transparent text-muted-foreground outline-none data-[hovered]:bg-muted data-[hovered]:text-foreground data-[focus-visible]:outline-solid data-[focus-visible]:outline-(length:--focus-ring-width) data-[focus-visible]:outline-offset-(--focus-ring-offset) data-[focus-visible]:outline-(--color-ring)",
147
+ children: /* @__PURE__ */ jsx(DismissGlyph, {})
148
+ }) : null
149
+ ]
62
150
  });
63
- }
151
+ if (!managed) return alert;
152
+ return /* @__PURE__ */ jsx("div", {
153
+ "data-slot": "alert-presence",
154
+ "data-shown": shown || void 0,
155
+ onTransitionEnd: (event) => {
156
+ if (leaving && event.target === event.currentTarget && event.propertyName === "grid-template-rows") finish();
157
+ },
158
+ className: "alert-presence",
159
+ children: /* @__PURE__ */ jsx("div", { children: alert })
160
+ });
161
+ });
64
162
  //#endregion
65
163
  export { Alert, alertVariants };
66
164