@seekalfred/primitives 0.5.1 → 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 (215) hide show
  1. package/README.md +664 -9
  2. package/dist/accordion/accordion.js +10 -4
  3. package/dist/accordion/accordion.js.map +1 -1
  4. package/dist/action-button/action-button.d.ts +50 -0
  5. package/dist/action-button/action-button.js +159 -0
  6. package/dist/action-button/action-button.js.map +1 -0
  7. package/dist/action-button/index.d.ts +2 -0
  8. package/dist/action-button/index.js +2 -0
  9. package/dist/alert/alert.d.ts +15 -1
  10. package/dist/alert/alert.js +116 -20
  11. package/dist/alert/alert.js.map +1 -1
  12. package/dist/avatar/avatar.d.ts +6 -0
  13. package/dist/avatar/avatar.js +47 -16
  14. package/dist/avatar/avatar.js.map +1 -1
  15. package/dist/badge/badge.d.ts +11 -4
  16. package/dist/badge/badge.js +35 -5
  17. package/dist/badge/badge.js.map +1 -1
  18. package/dist/breadcrumb/breadcrumb.d.ts +0 -27
  19. package/dist/breadcrumb/breadcrumb.js +20 -6
  20. package/dist/breadcrumb/breadcrumb.js.map +1 -1
  21. package/dist/button/button.d.ts +35 -22
  22. package/dist/button/button.js +177 -34
  23. package/dist/button/button.js.map +1 -1
  24. package/dist/button-group/button-group.d.ts +76 -0
  25. package/dist/button-group/button-group.js +403 -0
  26. package/dist/button-group/button-group.js.map +1 -0
  27. package/dist/button-group/index.d.ts +2 -0
  28. package/dist/button-group/index.js +2 -0
  29. package/dist/calendar/calendar.d.ts +1 -1
  30. package/dist/calendar/calendar.js +257 -30
  31. package/dist/calendar/calendar.js.map +1 -1
  32. package/dist/card/card.d.ts +9 -2
  33. package/dist/card/card.js +15 -4
  34. package/dist/card/card.js.map +1 -1
  35. package/dist/checkbox/checkbox.d.ts +0 -7
  36. package/dist/checkbox/checkbox.js +24 -27
  37. package/dist/checkbox/checkbox.js.map +1 -1
  38. package/dist/confirm-morph/confirm-morph.d.ts +85 -0
  39. package/dist/confirm-morph/confirm-morph.js +652 -0
  40. package/dist/confirm-morph/confirm-morph.js.map +1 -0
  41. package/dist/confirm-morph/index.d.ts +2 -0
  42. package/dist/confirm-morph/index.js +2 -0
  43. package/dist/copy-button/copy-button.d.ts +32 -0
  44. package/dist/copy-button/copy-button.js +134 -0
  45. package/dist/copy-button/copy-button.js.map +1 -0
  46. package/dist/copy-button/index.d.ts +2 -0
  47. package/dist/copy-button/index.js +2 -0
  48. package/dist/date-picker/date-picker.d.ts +51 -0
  49. package/dist/date-picker/date-picker.js +241 -0
  50. package/dist/date-picker/date-picker.js.map +1 -0
  51. package/dist/date-picker/date-range-picker.d.ts +78 -0
  52. package/dist/date-picker/date-range-picker.js +1015 -0
  53. package/dist/date-picker/date-range-picker.js.map +1 -0
  54. package/dist/date-picker/index.d.ts +4 -0
  55. package/dist/date-picker/index.js +4 -0
  56. package/dist/date-picker/picker-parts.d.ts +46 -0
  57. package/dist/date-picker/picker-parts.js +135 -0
  58. package/dist/date-picker/picker-parts.js.map +1 -0
  59. package/dist/dialog/dialog.d.ts +10 -14
  60. package/dist/dialog/dialog.js +39 -11
  61. package/dist/dialog/dialog.js.map +1 -1
  62. package/dist/empty-state/empty-state.d.ts +44 -0
  63. package/dist/empty-state/empty-state.js +83 -0
  64. package/dist/empty-state/empty-state.js.map +1 -0
  65. package/dist/empty-state/index.d.ts +2 -0
  66. package/dist/empty-state/index.js +2 -0
  67. package/dist/expanding-button-group/expanding-button-group.d.ts +54 -0
  68. package/dist/expanding-button-group/expanding-button-group.js +350 -0
  69. package/dist/expanding-button-group/expanding-button-group.js.map +1 -0
  70. package/dist/expanding-button-group/index.d.ts +2 -0
  71. package/dist/expanding-button-group/index.js +2 -0
  72. package/dist/file-drop-zone/file-drop-zone.d.ts +71 -0
  73. package/dist/file-drop-zone/file-drop-zone.js +116 -0
  74. package/dist/file-drop-zone/file-drop-zone.js.map +1 -0
  75. package/dist/file-drop-zone/index.d.ts +2 -0
  76. package/dist/file-drop-zone/index.js +2 -0
  77. package/dist/filter-bar/filter-bar.d.ts +121 -0
  78. package/dist/filter-bar/filter-bar.js +300 -0
  79. package/dist/filter-bar/filter-bar.js.map +1 -0
  80. package/dist/filter-bar/index.d.ts +2 -0
  81. package/dist/filter-bar/index.js +2 -0
  82. package/dist/floating-button-group/floating-button-group.d.ts +61 -0
  83. package/dist/floating-button-group/floating-button-group.js +340 -0
  84. package/dist/floating-button-group/floating-button-group.js.map +1 -0
  85. package/dist/floating-button-group/index.d.ts +2 -0
  86. package/dist/floating-button-group/index.js +2 -0
  87. package/dist/icon-morph/icon-morph.d.ts +55 -0
  88. package/dist/icon-morph/icon-morph.js +331 -0
  89. package/dist/icon-morph/icon-morph.js.map +1 -0
  90. package/dist/icon-morph/index.d.ts +2 -0
  91. package/dist/icon-morph/index.js +2 -0
  92. package/dist/index.d.ts +24 -2
  93. package/dist/index.js +29 -7
  94. package/dist/input/input.d.ts +22 -0
  95. package/dist/input/input.js +36 -12
  96. package/dist/input/input.js.map +1 -1
  97. package/dist/lib/count-to.js +113 -0
  98. package/dist/lib/count-to.js.map +1 -0
  99. package/dist/lib/eye-morph.js +62 -0
  100. package/dist/lib/eye-morph.js.map +1 -0
  101. package/dist/lib/field-message.js +336 -0
  102. package/dist/lib/field-message.js.map +1 -0
  103. package/dist/lib/field-shell.js +81 -0
  104. package/dist/lib/field-shell.js.map +1 -0
  105. package/dist/lib/field-surface.js +4 -2
  106. package/dist/lib/field-surface.js.map +1 -1
  107. package/dist/lib/flip.js +80 -0
  108. package/dist/lib/flip.js.map +1 -0
  109. package/dist/lib/height-follow.js +59 -0
  110. package/dist/lib/height-follow.js.map +1 -0
  111. package/dist/lib/icon-swap.js +47 -0
  112. package/dist/lib/icon-swap.js.map +1 -0
  113. package/dist/lib/leaving-list.js +57 -0
  114. package/dist/lib/leaving-list.js.map +1 -0
  115. package/dist/lib/merge-refs.js +3 -3
  116. package/dist/lib/merge-refs.js.map +1 -1
  117. package/dist/lib/morph-text.js +128 -0
  118. package/dist/lib/morph-text.js.map +1 -0
  119. package/dist/lib/motion.js +162 -0
  120. package/dist/lib/motion.js.map +1 -0
  121. package/dist/lib/pausable-timeout.js +67 -0
  122. package/dist/lib/pausable-timeout.js.map +1 -0
  123. package/dist/lib/rolling-value.js +31 -0
  124. package/dist/lib/rolling-value.js.map +1 -0
  125. package/dist/lib/spring-progress.js +114 -0
  126. package/dist/lib/spring-progress.js.map +1 -0
  127. package/dist/lib/svg-id.js +10 -0
  128. package/dist/lib/svg-id.js.map +1 -0
  129. package/dist/lib/swap-text.js +77 -0
  130. package/dist/lib/swap-text.js.map +1 -0
  131. package/dist/lib/use-copy-feedback.js +36 -0
  132. package/dist/lib/use-copy-feedback.js.map +1 -0
  133. package/dist/page-header/index.d.ts +2 -0
  134. package/dist/page-header/index.js +2 -0
  135. package/dist/page-header/page-header.d.ts +75 -0
  136. package/dist/page-header/page-header.js +83 -0
  137. package/dist/page-header/page-header.js.map +1 -0
  138. package/dist/password-field/index.d.ts +2 -0
  139. package/dist/password-field/index.js +2 -0
  140. package/dist/password-field/password-field.d.ts +37 -0
  141. package/dist/password-field/password-field.js +101 -0
  142. package/dist/password-field/password-field.js.map +1 -0
  143. package/dist/password-strength/index.d.ts +2 -0
  144. package/dist/password-strength/index.js +2 -0
  145. package/dist/password-strength/password-strength.d.ts +127 -0
  146. package/dist/password-strength/password-strength.js +455 -0
  147. package/dist/password-strength/password-strength.js.map +1 -0
  148. package/dist/popover/popover.d.ts +8 -0
  149. package/dist/popover/popover.js +3 -3
  150. package/dist/popover/popover.js.map +1 -1
  151. package/dist/progress/progress.js +46 -5
  152. package/dist/progress/progress.js.map +1 -1
  153. package/dist/radio-group/radio-group.js +7 -6
  154. package/dist/radio-group/radio-group.js.map +1 -1
  155. package/dist/scroll-area/scroll-area.d.ts +10 -0
  156. package/dist/scroll-area/scroll-area.js +25 -3
  157. package/dist/scroll-area/scroll-area.js.map +1 -1
  158. package/dist/search-field/index.d.ts +2 -0
  159. package/dist/search-field/index.js +2 -0
  160. package/dist/search-field/search-field.d.ts +26 -0
  161. package/dist/search-field/search-field.js +155 -0
  162. package/dist/search-field/search-field.js.map +1 -0
  163. package/dist/select/select.js +79 -21
  164. package/dist/select/select.js.map +1 -1
  165. package/dist/sheet/sheet.js +2 -1
  166. package/dist/sheet/sheet.js.map +1 -1
  167. package/dist/side-nav/index.d.ts +2 -0
  168. package/dist/side-nav/index.js +2 -0
  169. package/dist/side-nav/side-nav.d.ts +72 -0
  170. package/dist/side-nav/side-nav.js +142 -0
  171. package/dist/side-nav/side-nav.js.map +1 -0
  172. package/dist/skeleton/skeleton.d.ts +8 -2
  173. package/dist/skeleton/skeleton.js +6 -2
  174. package/dist/skeleton/skeleton.js.map +1 -1
  175. package/dist/slider/slider.js +61 -7
  176. package/dist/slider/slider.js.map +1 -1
  177. package/dist/split-button/index.d.ts +2 -0
  178. package/dist/split-button/index.js +2 -0
  179. package/dist/split-button/split-button.d.ts +46 -0
  180. package/dist/split-button/split-button.js +122 -0
  181. package/dist/split-button/split-button.js.map +1 -0
  182. package/dist/stat-card/index.d.ts +2 -0
  183. package/dist/stat-card/index.js +2 -0
  184. package/dist/stat-card/stat-card.d.ts +81 -0
  185. package/dist/stat-card/stat-card.js +152 -0
  186. package/dist/stat-card/stat-card.js.map +1 -0
  187. package/dist/styles.css +1 -1
  188. package/dist/switch/switch.js +9 -8
  189. package/dist/switch/switch.js.map +1 -1
  190. package/dist/table/index.d.ts +2 -2
  191. package/dist/table/index.js +2 -2
  192. package/dist/table/table.d.ts +36 -5
  193. package/dist/table/table.js +112 -11
  194. package/dist/table/table.js.map +1 -1
  195. package/dist/tabs/tabs.js +142 -6
  196. package/dist/tabs/tabs.js.map +1 -1
  197. package/dist/textarea/textarea.d.ts +23 -0
  198. package/dist/textarea/textarea.js +24 -2
  199. package/dist/textarea/textarea.js.map +1 -1
  200. package/dist/theme-switch/index.d.ts +2 -0
  201. package/dist/theme-switch/index.js +2 -0
  202. package/dist/theme-switch/theme-switch.d.ts +37 -0
  203. package/dist/theme-switch/theme-switch.js +123 -0
  204. package/dist/theme-switch/theme-switch.js.map +1 -0
  205. package/dist/time-picker/index.d.ts +2 -0
  206. package/dist/time-picker/index.js +2 -0
  207. package/dist/time-picker/time-picker.d.ts +54 -0
  208. package/dist/time-picker/time-picker.js +201 -0
  209. package/dist/time-picker/time-picker.js.map +1 -0
  210. package/dist/toast/toast.js +58 -38
  211. package/dist/toast/toast.js.map +1 -1
  212. package/dist/tooltip/tooltip.d.ts +1 -1
  213. package/dist/tooltip/tooltip.js +63 -14
  214. package/dist/tooltip/tooltip.js.map +1 -1
  215. package/package.json +83 -3
@@ -44,9 +44,10 @@ const accordionTriggerVariants = cva([
44
44
  "[&_svg]:pointer-events-none [&_svg]:shrink-0",
45
45
  "[&_svg:not([class*='size-'])]:size-(--icon-sm)",
46
46
  "[&_svg]:text-muted-foreground",
47
- "[&_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]"
48
49
  ]);
49
- const accordionPanelVariants = cva(["pb-(--density-control-pad-md) font-sans text-sm leading-(--leading-normal) text-foreground"]);
50
+ const accordionPanelVariants = cva(["pb-(--density-control-pad-md) font-sans text-sm leading-(--leading-normal) text-foreground", "accordion-panel"]);
50
51
  const Accordion = forwardRef(function Accordion({ className, ...props }, ref) {
51
52
  return /* @__PURE__ */ jsx(DisclosureGroup, {
52
53
  ref,
@@ -75,12 +76,17 @@ const AccordionTrigger = forwardRef(function AccordionTrigger({ children, icon,
75
76
  })
76
77
  });
77
78
  });
78
- const AccordionPanel = forwardRef(function AccordionPanel({ className, ...props }, ref) {
79
+ const AccordionPanel = forwardRef(function AccordionPanel({ className, children, ...props }, ref) {
79
80
  return /* @__PURE__ */ jsx(DisclosurePanel, {
80
81
  ref,
81
82
  ...props,
82
83
  "data-slot": "accordion-panel",
83
- 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
+ })
84
90
  });
85
91
  });
86
92
  //#endregion
@@ -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 { 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-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 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, ...props }, ref) {\n return (\n <AriaDisclosurePanel\n ref={ref}\n {...props}\n data-slot=\"accordion-panel\"\n className={cn(accordionPanelVariants(), className)}\n />\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,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,GAAG,SAAS,KAAK;CACpD,OACE,oBAACC,iBAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,uBAAuB,GAAG,SAAS;CAClD,CAAA;AAEL,CACF"}
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,6 +62,20 @@ 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
80
  export declare const Alert: import("react").ForwardRefExoticComponent<AlertProps & import("react").RefAttributes<HTMLDivElement>>;
67
81
  //#endregion
@@ -1,8 +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 { forwardRef } from "react";
5
- 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";
6
10
  //#region src/alert/alert.tsx
7
11
  /**
8
12
  * Alert, ported from the legacy shadcn component.
@@ -37,30 +41,122 @@ const alertVariants = cva([
37
41
  } },
38
42
  defaultVariants: { variant: "default" }
39
43
  });
40
- const Alert = forwardRef(function Alert({ className, variant, icon, title, headingLevel = 3, children, ...props }, ref) {
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) {
41
72
  const Title = `h${headingLevel}`;
42
- 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", {
43
104
  ref,
44
105
  ...props,
45
106
  role: "alert",
46
107
  "data-slot": "alert",
47
108
  className: cn(alertVariants({ variant }), className),
48
- children: [icon === void 0 ? null : /* @__PURE__ */ jsx("span", {
49
- "data-slot": "alert-icon",
50
- className: "shrink-0 [&>svg]:pointer-events-none [&>svg:not([class*='size-'])]:size-(--icon-sm)",
51
- children: icon
52
- }), /* @__PURE__ */ jsxs("div", {
53
- "data-slot": "alert-body",
54
- className: "min-w-0 flex-1",
55
- children: [title === void 0 ? null : /* @__PURE__ */ jsx(Title, {
56
- "data-slot": "alert-title",
57
- className: "mb-1 text-sm leading-(--leading-tight) font-medium",
58
- children: title
59
- }), children === void 0 ? null : /* @__PURE__ */ jsx("div", {
60
- "data-slot": "alert-description",
61
- children
62
- })]
63
- })]
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
+ ]
150
+ });
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 })
64
160
  });
65
161
  });
66
162
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"alert.js","names":[],"sources":["../../src/alert/alert.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { forwardRef, type ComponentPropsWithoutRef, type ReactNode } from \"react\";\n\nimport { cn } from \"../lib/cn.js\";\n\n/**\n * Alert, ported from the legacy shadcn component.\n *\n * There is no React Aria base: RAC ships no static inline alert (its Toast\n * API is transient and queue-driven), so this is a styled div with\n * role=\"alert\". Because it is not RAC-backed, its props follow\n * ARCHITECTURE section 7 rule 1 -- VariantProps & ComponentPropsWithoutRef --\n * rather than the Button's Omit-from-React-Aria departure, which exists only\n * to stop RAC-ignored props being advertised.\n *\n * Every string below is a STATIC literal. Tailwind scans source text, so a\n * class assembled at runtime is invisible to it and would never be emitted.\n *\n * THE LAYOUT IS THE BIGGEST CHANGE. The legacy component positioned its icon\n * with `[&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4`, pushed the text over\n * with `[&>svg~*]:pl-7`, and nudged it back with `[&>svg+div]:translate-y-[-3px]`.\n * All three are physical properties or magic numbers: `left` and `pl` do not\n * mirror in RTL, so the icon stayed left while the text indented the wrong\n * side, and `translate-y-[-3px]` is a bracket value that `check:hardcoding`\n * rejects outright. Flex with an explicit `icon` prop deletes all three\n * problems rather than porting them.\n */\nexport const alertVariants = cva(\n [\n // box-border is not boilerplate. Tailwind Preflight sets box-sizing\n // globally and the legacy app had it; this library's reset is OPT-IN, so\n // a consumer may load none and the browser default content-box applies.\n // w-full is width:100%, so the padding and border are then ADDED to it\n // and the alert overflows its container by exactly 46px at comfortable\n // density. Measured, not theorised: 346px inside a 300px parent.\n \"box-border flex w-full items-start gap-(--density-control-gap)\",\n \"rounded-lg border-(length:--border-width) p-(--density-control-pad-md)\",\n // font-sans is load-bearing. The library does not style the consumer's\n // page, so anything that does not set its own face inherits the host's\n // default -- which is a serif in an unstyled document. The accordion\n // panel shipped exactly that way and it was caught by eye, not by a test.\n \"font-sans text-sm leading-(--leading-normal)\",\n ],\n {\n variants: {\n variant: {\n default: \"bg-background text-foreground border-border\",\n // destructive-STRONG, not destructive. The fill token is 4.04:1 on\n // the dark page; this role exists so the text and border clear the\n // 4.5 floor in both themes. The legacy `border-destructive/50` was\n // also an opacity modifier, which cannot be contrast-checked at all\n // because the result depends on whatever sits behind it.\n destructive: \"bg-background text-destructive-strong border-destructive-strong\",\n },\n },\n defaultVariants: { variant: \"default\" },\n },\n);\n\n/**\n * An inline status message.\n *\n * Announced by assistive technology the moment it renders, via `role=\"alert\"`.\n * That makes it the wrong component for content that is present on load and\n * merely styled as a notice -- use a plain region for those.\n *\n * @example\n * <Alert variant=\"destructive\" icon={<WarningIcon aria-hidden />} title=\"Save failed\">\n * Your changes were not saved.\n * </Alert>\n */\nexport interface AlertProps\n // The native `title` attribute is omitted so this component can use the\n // name for its heading. That attribute renders a browser tooltip, which is\n // not keyboard reachable and is not announced reliably -- on a role=\"alert\"\n // it is worth nothing, and silently shadowing it with a ReactNode would be\n // a type error waiting for the first consumer who tried both.\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"title\">, VariantProps<typeof alertVariants> {\n /**\n * Leading glyph. Mark it `aria-hidden` -- the message text is the\n * announcement, and an unhidden graphic makes the alert announce twice.\n *\n * A prop rather than a positioned child: the legacy `[&>svg]` selectors\n * only worked when the icon was the first element, and broke in RTL.\n */\n icon?: ReactNode;\n /**\n * Optional heading. Replaces the legacy `AlertTitle` part.\n *\n * Omit it for a single-line alert; most call sites do.\n */\n title?: ReactNode;\n /**\n * Heading level for {@link AlertProps.title}.\n *\n * The legacy `AlertTitle` was always an `<h5>`, which breaks the document\n * outline anywhere the alert is not nested five deep.\n *\n * @default 3\n */\n headingLevel?: 2 | 3 | 4 | 5 | 6;\n}\n\nexport const Alert = forwardRef<HTMLDivElement, AlertProps>(function Alert(\n { className, variant, icon, title, headingLevel = 3, children, ...props },\n ref,\n) {\n const Title = `h${headingLevel}` as const;\n\n return (\n <div\n ref={ref}\n {...props}\n role=\"alert\"\n data-slot=\"alert\"\n className={cn(alertVariants({ variant }), className)}\n >\n {/*\n * The glyph is sized from the icon scale unless the caller sized it,\n * and never becomes a press target. shrink-0 keeps it square when the\n * message wraps.\n */}\n {icon === undefined ? null : (\n <span\n data-slot=\"alert-icon\"\n className=\"shrink-0 [&>svg]:pointer-events-none [&>svg:not([class*='size-'])]:size-(--icon-sm)\"\n >\n {icon}\n </span>\n )}\n <div data-slot=\"alert-body\" className=\"min-w-0 flex-1\">\n {title === undefined ? null : (\n // leading-* AFTER text-*, always: tailwind-merge treats a later\n // text-* as owning line-height and silently strips a leading-*\n // that precedes it.\n <Title\n data-slot=\"alert-title\"\n className=\"mb-1 text-sm leading-(--leading-tight) font-medium\"\n >\n {title}\n </Title>\n )}\n {children === undefined ? null : <div data-slot=\"alert-description\">{children}</div>}\n </div>\n </div>\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,MAAa,gBAAgB,IAC3B;CAOE;CACA;CAKA;AACF,GACA;CACE,UAAU,EACR,SAAS;EACP,SAAS;EAMT,aAAa;CACf,EACF;CACA,iBAAiB,EAAE,SAAS,UAAU;AACxC,CACF;AA8CA,MAAa,QAAQ,WAAuC,SAAS,MACnE,EAAE,WAAW,SAAS,MAAM,OAAO,eAAe,GAAG,UAAU,GAAG,SAClE,KACA;CACA,MAAM,QAAQ,IAAI;CAElB,OACE,qBAAC,OAAD;EACO;EACL,GAAI;EACJ,MAAK;EACL,aAAU;EACV,WAAW,GAAG,cAAc,EAAE,QAAQ,CAAC,GAAG,SAAS;EALrD,UAAA,CAYG,SAAS,KAAA,IAAY,OACpB,oBAAC,QAAD;GACE,aAAU;GACV,WAAU;GAET,UAAA;EACG,CAAA,GAER,qBAAC,OAAD;GAAK,aAAU;GAAa,WAAU;GAAtC,UAAA,CACG,UAAU,KAAA,IAAY,OAIrB,oBAAC,OAAD;IACE,aAAU;IACV,WAAU;IAET,UAAA;GACI,CAAA,GAER,aAAa,KAAA,IAAY,OAAO,oBAAC,OAAD;IAAK,aAAU;IAAqB;GAAc,CAAA,CAChF;EACF,CAAA,CAAA;;AAET,CAAC"}
1
+ {"version":3,"file":"alert.js","names":["AriaButton"],"sources":["../../src/alert/alert.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport {\n forwardRef,\n isValidElement,\n useCallback,\n useEffect,\n useRef,\n useState,\n type ComponentPropsWithoutRef,\n type ReactNode,\n} from \"react\";\n\nimport { Button as AriaButton } from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { useHeightFollow } from \"../lib/height-follow.js\";\nimport { prefersReducedMotion, useExitBackstop, useIsomorphicLayoutEffect } from \"../lib/motion.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\n\n/**\n * Alert, ported from the legacy shadcn component.\n *\n * There is no React Aria base: RAC ships no static inline alert (its Toast\n * API is transient and queue-driven), so this is a styled div with\n * role=\"alert\". Because it is not RAC-backed, its props follow\n * ARCHITECTURE section 7 rule 1 -- VariantProps & ComponentPropsWithoutRef --\n * rather than the Button's Omit-from-React-Aria departure, which exists only\n * to stop RAC-ignored props being advertised.\n *\n * Every string below is a STATIC literal. Tailwind scans source text, so a\n * class assembled at runtime is invisible to it and would never be emitted.\n *\n * THE LAYOUT IS THE BIGGEST CHANGE. The legacy component positioned its icon\n * with `[&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4`, pushed the text over\n * with `[&>svg~*]:pl-7`, and nudged it back with `[&>svg+div]:translate-y-[-3px]`.\n * All three are physical properties or magic numbers: `left` and `pl` do not\n * mirror in RTL, so the icon stayed left while the text indented the wrong\n * side, and `translate-y-[-3px]` is a bracket value that `check:hardcoding`\n * rejects outright. Flex with an explicit `icon` prop deletes all three\n * problems rather than porting them.\n */\nexport const alertVariants = cva(\n [\n // box-border is not boilerplate. Tailwind Preflight sets box-sizing\n // globally and the legacy app had it; this library's reset is OPT-IN, so\n // a consumer may load none and the browser default content-box applies.\n // w-full is width:100%, so the padding and border are then ADDED to it\n // and the alert overflows its container by exactly 46px at comfortable\n // density. Measured, not theorised: 346px inside a 300px parent.\n \"box-border flex w-full items-start gap-(--density-control-gap)\",\n \"rounded-lg border-(length:--border-width) p-(--density-control-pad-md)\",\n // font-sans is load-bearing. The library does not style the consumer's\n // page, so anything that does not set its own face inherits the host's\n // default -- which is a serif in an unstyled document. The accordion\n // panel shipped exactly that way and it was caught by eye, not by a test.\n \"font-sans text-sm leading-(--leading-normal)\",\n ],\n {\n variants: {\n variant: {\n default: \"bg-background text-foreground border-border\",\n // destructive-STRONG, not destructive. The fill token is 4.04:1 on\n // the dark page; this role exists so the text and border clear the\n // 4.5 floor in both themes. The legacy `border-destructive/50` was\n // also an opacity modifier, which cannot be contrast-checked at all\n // because the result depends on whatever sits behind it.\n destructive: \"bg-background text-destructive-strong border-destructive-strong\",\n },\n },\n defaultVariants: { variant: \"default\" },\n },\n);\n\n/**\n * An inline status message.\n *\n * Announced by assistive technology the moment it renders, via `role=\"alert\"`.\n * That makes it the wrong component for content that is present on load and\n * merely styled as a notice -- use a plain region for those.\n *\n * @example\n * <Alert variant=\"destructive\" icon={<WarningIcon aria-hidden />} title=\"Save failed\">\n * Your changes were not saved.\n * </Alert>\n */\nexport interface AlertProps\n // The native `title` attribute is omitted so this component can use the\n // name for its heading. That attribute renders a browser tooltip, which is\n // not keyboard reachable and is not announced reliably -- on a role=\"alert\"\n // it is worth nothing, and silently shadowing it with a ReactNode would be\n // a type error waiting for the first consumer who tried both.\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"title\">, VariantProps<typeof alertVariants> {\n /**\n * Leading glyph. Mark it `aria-hidden` -- the message text is the\n * announcement, and an unhidden graphic makes the alert announce twice.\n *\n * A prop rather than a positioned child: the legacy `[&>svg]` selectors\n * only worked when the icon was the first element, and broke in RTL.\n */\n icon?: ReactNode;\n /**\n * Optional heading. Replaces the legacy `AlertTitle` part.\n *\n * Omit it for a single-line alert; most call sites do.\n */\n title?: ReactNode;\n /**\n * Heading level for {@link AlertProps.title}.\n *\n * The legacy `AlertTitle` was always an `<h5>`, which breaks the document\n * outline anywhere the alert is not nested five deep.\n *\n * @default 3\n */\n headingLevel?: 2 | 3 | 4 | 5 | 6;\n /**\n * Whether the alert is shown. Controlled: hiding it collapses its height and\n * fades it out, and showing it again grows it back. An alert that is open on\n * first render does not animate. Leave it out for an alert that is always there.\n */\n open?: boolean;\n /**\n * Adds a Dismiss button. Pressed, a controlled alert (`open` given) only calls\n * this and waits to be told `open={false}`; an uncontrolled alert (no `open`)\n * collapses first and then calls it.\n */\n onDismiss?: () => void;\n /** The Dismiss button's accessible name. @default \"Dismiss\" */\n dismissLabel?: string;\n}\n\n/** A string title or description swaps as a whole when its text changes. */\nfunction Swappable({ value, children }: { value: ReactNode; children: ReactNode }) {\n if (typeof value !== \"string\" && typeof value !== \"number\") return <>{children}</>;\n return (\n <SwapText swapKey={String(value)} className=\"block\">\n {children}\n </SwapText>\n );\n}\n\n/** The name an icon swaps under: a different icon component crossfades; the same one re-rendering stays still. */\nfunction iconKey(icon: ReactNode) {\n if (!isValidElement(icon)) return \"icon\";\n const type = icon.type as string | { displayName?: string; name?: string };\n return typeof type === \"string\" ? type : (type.displayName ?? type.name ?? \"icon\");\n}\n\nfunction DismissGlyph() {\n return (\n <svg\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n aria-hidden=\"true\"\n className=\"size-4\"\n >\n <path d=\"M4 4l8 8M12 4l-8 8\" />\n </svg>\n );\n}\n\nexport const Alert = forwardRef<HTMLDivElement, AlertProps>(function Alert(\n {\n className,\n variant,\n icon,\n title,\n headingLevel = 3,\n children,\n open,\n onDismiss,\n dismissLabel = \"Dismiss\",\n ...props\n },\n ref,\n) {\n const Title = `h${headingLevel}` as const;\n const frame = useRef<HTMLDivElement>(null);\n const content = useRef<HTMLDivElement>(null);\n const text = typeof children === \"string\" || typeof children === \"number\" ? String(children) : \"\";\n useHeightFollow(\n frame,\n content,\n `${typeof title === \"string\" ? title : \"\"}\\n${children === undefined ? \"\" : \"details\"}\\n${text}`,\n );\n\n // Presence. Without `open` or `onDismiss` the alert is simply there.\n const managed = open !== undefined || onDismiss !== undefined;\n const [dismissed, setDismissed] = useState(false);\n const wanted = open ?? !dismissed;\n const [rendered, setRendered] = useState(wanted);\n const [shown, setShown] = useState(wanted);\n if (wanted && !rendered) setRendered(true);\n if (!wanted && shown) setShown(false);\n useEffect(() => {\n // A newly shown alert mounts collapsed and opens a frame later, so it grows rather than appears.\n if (wanted && rendered && !shown) {\n const frameId = requestAnimationFrame(() => setShown(true));\n return () => cancelAnimationFrame(frameId);\n }\n }, [wanted, rendered, shown]);\n const leaving = rendered && !wanted;\n const finish = useCallback(() => {\n setRendered(false);\n if (open === undefined) onDismiss?.();\n }, [onDismiss, open]);\n useExitBackstop(leaving, finish, 900);\n useIsomorphicLayoutEffect(() => {\n if (leaving && prefersReducedMotion()) finish();\n }, [leaving, finish]);\n if (managed && !rendered) return null;\n\n const alert = (\n <div\n ref={ref}\n {...props}\n role=\"alert\"\n data-slot=\"alert\"\n className={cn(alertVariants({ variant }), className)}\n >\n {/*\n The icon is decorative or labelled by the consumer; this wrapper adds\n no name. It keeps the icon from shrinking and sizes a bare svg to the\n icon scale, unless the consumer sized it themselves.\n */}\n {icon === undefined ? null : (\n <span\n data-slot=\"alert-icon\"\n className=\"grid shrink-0 [&>svg]:pointer-events-none [&>svg:not([class*='size-'])]:size-(--icon-sm)\"\n >\n <SwapText swapKey={iconKey(icon)} motion=\"glyph\" className=\"grid place-items-center\">\n {icon}\n </SwapText>\n </span>\n )}\n {/* The copy column follows its content's height, so longer or shorter text never snaps the alert. */}\n <div ref={frame} data-slot=\"alert-body\" className=\"height-follow min-w-0 flex-1\">\n <div ref={content}>\n {title === undefined ? null : (\n <Title\n data-slot=\"alert-title\"\n className=\"mb-1 text-sm leading-(--leading-tight) font-medium\"\n >\n <Swappable value={title}>{title}</Swappable>\n </Title>\n )}\n {children === undefined ? null : (\n <div data-slot=\"alert-description\">\n <Swappable value={children}>{children}</Swappable>\n </div>\n )}\n </div>\n </div>\n {onDismiss ? (\n <AriaButton\n data-slot=\"alert-dismiss\"\n aria-label={typeof title === \"string\" ? `${dismissLabel}: ${title}` : dismissLabel}\n onPress={() => (open === undefined ? setDismissed(true) : onDismiss())}\n 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)\"\n >\n <DismissGlyph />\n </AriaButton>\n ) : null}\n </div>\n );\n if (!managed) return alert;\n return (\n <div\n data-slot=\"alert-presence\"\n data-shown={shown || undefined}\n onTransitionEnd={(event) => {\n if (leaving && event.target === event.currentTarget && event.propertyName === \"grid-template-rows\")\n finish();\n }}\n className=\"alert-presence\"\n >\n <div>{alert}</div>\n </div>\n );\n});\n"],"mappings":"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"}
@@ -29,6 +29,12 @@ export declare const avatarVariants: (props?: ({
29
29
  export interface AvatarProps extends Omit<ComponentPropsWithoutRef<"span">, "children">, VariantProps<typeof avatarVariants> {
30
30
  /** Image URL. Omit it to render the fallback alone. */
31
31
  src?: string;
32
+ /**
33
+ * A small dot at the corner: `online` or `offline`. It grows in when it appears,
34
+ * shrinks away when it goes, and swaps when it changes. Its word is read to
35
+ * assistive technology beside the name.
36
+ */
37
+ presence?: "online" | "offline";
32
38
  /**
33
39
  * Alternative text. **Required**, and `""` is the right value when the
34
40
  * person's name already appears next to the avatar.