@sableui/react 0.13.2 → 0.15.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 (203) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +172 -0
  3. package/CONTRIBUTING.md +6 -4
  4. package/DESIGN.md +10 -3
  5. package/dist/blocks/approval-block.js +93 -0
  6. package/dist/blocks/approval-block.meta.js +23 -0
  7. package/dist/blocks/index.d.ts +27 -1
  8. package/dist/blocks/index.js +8 -0
  9. package/dist/blocks/message-reactions-block.js +87 -0
  10. package/dist/blocks/message-reactions-block.meta.js +23 -0
  11. package/dist/components/accordion/accordion.meta.js +8 -2
  12. package/dist/components/area-chart/area-chart.meta.js +10 -3
  13. package/dist/components/attachment/attachment.meta.js +26 -0
  14. package/dist/components/bar-chart/bar-chart.meta.js +11 -4
  15. package/dist/components/bottom-nav/bottom-nav.js +3 -1
  16. package/dist/components/bottom-nav/bottom-nav.meta.js +32 -0
  17. package/dist/components/breadcrumbs/breadcrumbs.meta.js +26 -0
  18. package/dist/components/bubble/bubble.js +105 -5
  19. package/dist/components/bubble/bubble.meta.js +42 -6
  20. package/dist/components/bubble/bubble.variants.js +1 -1
  21. package/dist/components/bubble/index.js +2 -0
  22. package/dist/components/button/button.js +21 -4
  23. package/dist/components/button/button.meta.js +7 -7
  24. package/dist/components/calendar/calendar.js +18 -1
  25. package/dist/components/calendar/calendar.meta.js +38 -0
  26. package/dist/components/carousel/carousel.js +6 -4
  27. package/dist/components/carousel/carousel.meta.js +42 -0
  28. package/dist/components/chat-composer/chat-composer.js +21 -21
  29. package/dist/components/chat-composer/chat-composer.meta.js +38 -0
  30. package/dist/components/chat-composer/use-eased-growth.js +70 -0
  31. package/dist/components/checkbox/checkbox-mark.js +8 -1
  32. package/dist/components/checkbox/checkbox.js +3 -1
  33. package/dist/components/checkbox/checkbox.meta.js +32 -0
  34. package/dist/components/checkbox-card/checkbox-card.js +3 -0
  35. package/dist/components/checkbox-card/checkbox-card.meta.js +32 -0
  36. package/dist/components/code-block/code-block.js +14 -3
  37. package/dist/components/code-block/code-block.meta.js +28 -1
  38. package/dist/components/collapsible/collapsible.meta.js +32 -0
  39. package/dist/components/color-field/color-field.meta.js +26 -0
  40. package/dist/components/combobox/combobox.meta.js +44 -0
  41. package/dist/components/command-menu/command-menu.js +8 -3
  42. package/dist/components/command-menu/command-menu.meta.js +38 -0
  43. package/dist/components/confirm-button/confirm-button.js +179 -0
  44. package/dist/components/confirm-button/confirm-button.meta.js +143 -0
  45. package/dist/components/confirm-button/index.js +7 -0
  46. package/dist/components/data-table/data-table.js +6 -2
  47. package/dist/components/data-table/data-table.meta.js +32 -0
  48. package/dist/components/entity/entity.meta.js +26 -0
  49. package/dist/components/feedback/feedback.js +120 -47
  50. package/dist/components/feedback/feedback.meta.js +55 -4
  51. package/dist/components/input-group/input-group.meta.js +27 -1
  52. package/dist/components/like-button/index.js +7 -0
  53. package/dist/components/like-button/like-button.js +89 -0
  54. package/dist/components/like-button/like-button.meta.js +132 -0
  55. package/dist/components/line-chart/line-chart.meta.js +10 -3
  56. package/dist/components/middle-truncate/middle-truncate.meta.js +1 -1
  57. package/dist/components/note/note.meta.js +32 -0
  58. package/dist/components/notification-badge/notification-badge.js +17 -5
  59. package/dist/components/notification-badge/notification-badge.meta.js +34 -2
  60. package/dist/components/number-ticker/index.js +7 -0
  61. package/dist/components/number-ticker/number-ticker.js +246 -0
  62. package/dist/components/number-ticker/number-ticker.meta.js +159 -0
  63. package/dist/components/otp-field/otp-field.js +23 -3
  64. package/dist/components/otp-field/otp-field.meta.js +23 -2
  65. package/dist/components/panel-group/panel-group.meta.js +26 -0
  66. package/dist/components/panel-section/panel-section.js +37 -10
  67. package/dist/components/panel-section/panel-section.meta.js +32 -0
  68. package/dist/components/pie-chart/pie-chart.js +20 -11
  69. package/dist/components/pie-chart/pie-chart.meta.js +17 -4
  70. package/dist/components/pill/pill.meta.js +32 -0
  71. package/dist/components/radar-chart/radar-chart.js +4 -3
  72. package/dist/components/radar-chart/radar-chart.meta.js +10 -3
  73. package/dist/components/radial-chart/radial-chart.js +22 -22
  74. package/dist/components/radial-chart/radial-chart.meta.js +17 -4
  75. package/dist/components/radio/radio.meta.js +26 -0
  76. package/dist/components/radio-card/radio-card.meta.js +38 -0
  77. package/dist/components/reaction-picker/index.js +13 -0
  78. package/dist/components/reaction-picker/reaction-picker.js +104 -0
  79. package/dist/components/reaction-picker/reaction-picker.meta.js +156 -0
  80. package/dist/components/relative-time/relative-time.meta.js +32 -0
  81. package/dist/components/scroller/scroller.meta.js +32 -0
  82. package/dist/components/scrub-field/scrub-field.meta.js +32 -0
  83. package/dist/components/search-input/search-input.js +9 -7
  84. package/dist/components/search-input/search-input.meta.js +26 -0
  85. package/dist/components/show-more/show-more.js +1 -4
  86. package/dist/components/show-more/show-more.meta.js +38 -0
  87. package/dist/components/side-nav/side-nav.js +21 -1
  88. package/dist/components/side-nav/side-nav.meta.js +32 -0
  89. package/dist/components/side-nav/side-nav.variants.js +3 -1
  90. package/dist/components/sidebar/sidebar.js +31 -8
  91. package/dist/components/sidebar/sidebar.meta.js +38 -0
  92. package/dist/components/slider/slider.meta.js +26 -0
  93. package/dist/components/sparkline/sparkline.js +7 -4
  94. package/dist/components/sparkline/sparkline.meta.js +9 -2
  95. package/dist/components/table/table.meta.js +26 -0
  96. package/dist/components/tabs/tabs.js +27 -8
  97. package/dist/components/tabs/tabs.meta.js +46 -3
  98. package/dist/components/theme-selector/theme-selector.meta.js +32 -0
  99. package/dist/components/theme-switcher/theme-switcher.js +4 -1
  100. package/dist/components/theme-switcher/theme-switcher.meta.js +42 -0
  101. package/dist/components/toast/toast.js +25 -13
  102. package/dist/components/toast/toast.meta.js +7 -1
  103. package/dist/components/toggle/toggle.js +28 -5
  104. package/dist/components/toggle/toggle.meta.js +44 -2
  105. package/dist/components/tool-call/index.js +8 -0
  106. package/dist/components/tool-call/tool-call.js +117 -0
  107. package/dist/components/tool-call/tool-call.meta.js +147 -0
  108. package/dist/components/tool-call/tool-call.variants.js +33 -0
  109. package/dist/components/tooltip/index.js +3 -1
  110. package/dist/components/tooltip/tooltip.js +84 -0
  111. package/dist/components/tooltip/tooltip.meta.js +16 -1
  112. package/dist/components/tree/tree.js +71 -6
  113. package/dist/components/tree/tree.meta.js +32 -0
  114. package/dist/components/video/video.meta.js +32 -0
  115. package/dist/file-hashes.json +130 -104
  116. package/dist/foundations/motion.js +69 -3
  117. package/dist/icon-sets/reicon.js +11 -1
  118. package/dist/index.d.ts +907 -104
  119. package/dist/index.js +6 -1
  120. package/dist/lib/chart/cartesian.js +9 -10
  121. package/dist/lib/chart/chart-center.js +30 -0
  122. package/dist/lib/chart/chart-frame.js +15 -8
  123. package/dist/lib/chart/dim.js +13 -0
  124. package/dist/lib/chart/format.js +7 -1
  125. package/dist/lib/chart/motion.js +6 -4
  126. package/dist/lib/highlight-glide.js +68 -10
  127. package/dist/lib/taxonomy.js +5 -0
  128. package/dist/motion/Burst.js +79 -0
  129. package/dist/motion/MotionProvider.js +45 -3
  130. package/dist/motion/Reveal.js +1 -1
  131. package/dist/motion/TextReveal.js +1 -1
  132. package/dist/motion/index.js +17 -2
  133. package/dist/motion/useAutoSize.js +42 -0
  134. package/dist/motion/useShake.js +25 -0
  135. package/dist/styles.css +439 -5
  136. package/llms/blocks/approval-block.md +128 -0
  137. package/llms/blocks/message-reactions-block.md +116 -0
  138. package/llms/components/accordion.md +3 -2
  139. package/llms/components/area-chart.md +4 -3
  140. package/llms/components/attachment.md +7 -0
  141. package/llms/components/bar-chart.md +5 -4
  142. package/llms/components/bottom-nav.md +8 -0
  143. package/llms/components/breadcrumbs.md +7 -0
  144. package/llms/components/bubble.md +17 -7
  145. package/llms/components/button.md +6 -6
  146. package/llms/components/calendar.md +9 -0
  147. package/llms/components/carousel.md +17 -0
  148. package/llms/components/chat-composer.md +9 -0
  149. package/llms/components/checkbox-card.md +8 -0
  150. package/llms/components/checkbox.md +8 -0
  151. package/llms/components/code-block.md +8 -0
  152. package/llms/components/collapsible.md +8 -0
  153. package/llms/components/color-field.md +7 -0
  154. package/llms/components/combobox.md +10 -0
  155. package/llms/components/command-menu.md +9 -0
  156. package/llms/components/confirm-button.md +92 -0
  157. package/llms/components/data-table.md +8 -0
  158. package/llms/components/entity.md +7 -0
  159. package/llms/components/feedback.md +23 -3
  160. package/llms/components/input-group.md +8 -1
  161. package/llms/components/like-button.md +91 -0
  162. package/llms/components/line-chart.md +4 -3
  163. package/llms/components/middle-truncate.md +1 -1
  164. package/llms/components/note.md +8 -0
  165. package/llms/components/notification-badge.md +10 -2
  166. package/llms/components/number-ticker.md +86 -0
  167. package/llms/components/otp-field.md +15 -1
  168. package/llms/components/panel-group.md +7 -0
  169. package/llms/components/panel-section.md +8 -0
  170. package/llms/components/pie-chart.md +6 -4
  171. package/llms/components/pill.md +8 -0
  172. package/llms/components/radar-chart.md +4 -3
  173. package/llms/components/radial-chart.md +6 -4
  174. package/llms/components/radio-card.md +9 -0
  175. package/llms/components/radio.md +7 -0
  176. package/llms/components/reaction-picker.md +115 -0
  177. package/llms/components/relative-time.md +8 -0
  178. package/llms/components/scroller.md +8 -0
  179. package/llms/components/scrub-field.md +8 -0
  180. package/llms/components/search-input.md +7 -0
  181. package/llms/components/show-more.md +9 -0
  182. package/llms/components/side-nav.md +8 -0
  183. package/llms/components/sidebar.md +9 -0
  184. package/llms/components/slider.md +7 -0
  185. package/llms/components/sparkline.md +3 -2
  186. package/llms/components/table.md +7 -0
  187. package/llms/components/tabs.md +20 -2
  188. package/llms/components/theme-selector.md +8 -0
  189. package/llms/components/theme-switcher.md +17 -0
  190. package/llms/components/toast.md +2 -1
  191. package/llms/components/toggle.md +19 -2
  192. package/llms/components/tool-call.md +115 -0
  193. package/llms/components/tooltip.md +11 -1
  194. package/llms/components/tree.md +8 -0
  195. package/llms/components/video.md +8 -0
  196. package/llms/foundations/motion.md +64 -3
  197. package/llms/reference/utilities.md +26 -1
  198. package/llms/tasks/reviewing-motion.md +14 -0
  199. package/llms-full.txt +1337 -69
  200. package/llms.txt +19 -11
  201. package/package.json +1 -1
  202. package/registry.json +3040 -124
  203. package/targets/ios.json +45 -0
@@ -3,6 +3,7 @@
3
3
  // src/components/calendar/calendar.tsx
4
4
  import { Icon } from "../../icon/index.js";
5
5
  import { cn } from "../../lib/cn.js";
6
+ import { motionAttribute, useMotionLevel } from "../../motion/MotionProvider.js";
6
7
  import { buttonVariants } from "../button/button.variants.js";
7
8
  import {
8
9
  DayPicker
@@ -23,13 +24,18 @@ function Calendar({
23
24
  showOutsideDays = true,
24
25
  components,
25
26
  bare = false,
27
+ motion,
28
+ animate,
26
29
  ...props
27
30
  }) {
31
+ const level = useMotionLevel(motion);
28
32
  return /* @__PURE__ */ jsx(
29
33
  DayPicker,
30
34
  {
31
35
  "data-slot": "calendar",
36
+ "data-motion": motionAttribute(motion),
32
37
  showOutsideDays,
38
+ animate: animate ?? level.expressive,
33
39
  className: cn(
34
40
  // relative: the nav is absolutely positioned (react-day-picker v9
35
41
  // renders it inside `months`, not inside each month), and with no
@@ -53,7 +59,10 @@ function Calendar({
53
59
  weekdays: "flex",
54
60
  weekday: "flex size-9 items-center justify-center text-caption text-muted-foreground",
55
61
  week: "mt-1 flex w-full",
56
- day: "p-0 text-center",
62
+ // motion-interactive on the cell too: a range's fill is painted on the
63
+ // cells (see range_* below), so without it the band between the
64
+ // endpoints snapped while the endpoint buttons eased.
65
+ day: "p-0 text-center motion-interactive",
57
66
  day_button: "inline-flex size-9 items-center justify-center rounded-md text-body-sm motion-interactive outline-none hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring",
58
67
  // Range fill lives on the CELL so it forms one continuous bar; only the
59
68
  // outer ends are rounded. The selected (primary) endpoint buttons sit on
@@ -76,6 +85,14 @@ function Calendar({
76
85
  outside: "[&>button]:text-muted-foreground",
77
86
  disabled: "[&>button]:pointer-events-none [&>button]:text-muted-foreground [&>button]:opacity-50",
78
87
  hidden: "invisible",
88
+ weeks_after_enter: "sable-month-enter-after",
89
+ weeks_before_enter: "sable-month-enter-before",
90
+ weeks_after_exit: "sable-month-exit-after",
91
+ weeks_before_exit: "sable-month-exit-before",
92
+ caption_after_enter: "sable-caption-enter",
93
+ caption_before_enter: "sable-caption-enter",
94
+ caption_after_exit: "sable-caption-exit",
95
+ caption_before_exit: "sable-caption-exit",
79
96
  ...classNames
80
97
  },
81
98
  components: { Chevron: CalendarChevron, ...components },
@@ -84,6 +84,12 @@ var calendarMeta = {
84
84
  default: "false",
85
85
  description: "Drop the card frame (border, fill, padding) so the grid sits flush inside a surface that already has one, such as a Popover. DatePicker renders its Calendar this way."
86
86
  },
87
+ {
88
+ name: "motion",
89
+ type: '"full" | "subtle" | "off"',
90
+ default: '"full"',
91
+ description: "How much motion this component uses: `full` inherits the app's policy, `subtle` keeps feedback and drops the month slide, `off` stills it. It can only turn motion down \u2014 it never overrides a reader's reduce-motion setting."
92
+ },
87
93
  {
88
94
  name: "className",
89
95
  type: "string",
@@ -114,6 +120,38 @@ var calendarMeta = {
114
120
  "muted-foreground",
115
121
  "ring"
116
122
  ],
123
+ motion: [
124
+ {
125
+ slot: "arrival",
126
+ recipe: "none",
127
+ part: "Root",
128
+ why: "Inline, it arrives with its form; in a DatePicker the Popover carries the entrance. A grid that animated in by itself would put a second motion on the same opening."
129
+ },
130
+ {
131
+ slot: "response",
132
+ recipe: "motion-interactive",
133
+ part: "Day",
134
+ why: "Days ease their hover wash, and the month arrows are ghost Buttons on Button's own recipe. Across a seven-column grid the pointer crosses many cells at once, and an eased wash reads as a cursor travelling rather than a grid flashing."
135
+ },
136
+ {
137
+ slot: "transition",
138
+ recipe: "motion-interactive",
139
+ part: "Day",
140
+ why: "Picking a day eases the primary fill onto it and off the previous one, so a click reads as the selection moving rather than two cells flipping. A range's band is painted on the cells, which carry the same recipe, so the band eases along with its endpoints as the range grows."
141
+ },
142
+ {
143
+ slot: "activity",
144
+ recipe: "none",
145
+ part: "Root",
146
+ why: "Days are resident data; nothing in the grid loads. Availability fetched per month belongs to the surface around the calendar."
147
+ },
148
+ {
149
+ slot: "transition",
150
+ recipe: "custom",
151
+ part: "Month",
152
+ why: "Paging months slides the outgoing grid toward the edge it is leaving by and brings the new one in from the other, over a short travel with a fade (react-day-picker's `animate` mode on Sable keyframes in styles.css), so forward and back read as direction rather than a grid swapping in one frame. RDP adds a single class per part, which a recipe utility cannot be; mirrored in RTL. Expressive: Calendar turns `animate` on only at full motion, and an explicit `animate` prop still wins."
153
+ }
154
+ ],
117
155
  a11y: [
118
156
  "react-day-picker manages the roving-tabindex grid, arrow-key navigation, and ARIA roles/labels for the calendar.",
119
157
  "Selected days use bg-primary/text-primary-foreground for AA contrast; today is marked with a dot under the number (not a ring, which is reserved for keyboard focus) rather than color alone.",
@@ -14,7 +14,7 @@ import { Icon } from "../../icon/Icon.js";
14
14
  import { cn } from "../../lib/cn.js";
15
15
  import { PlayPauseGlyph } from "../../lib/glyphs.js";
16
16
  import { useResizeObserver } from "../../lib/use-resize-observer.js";
17
- import { useMotion } from "../../motion/MotionProvider.js";
17
+ import { useMotionLevel } from "../../motion/MotionProvider.js";
18
18
  import { Button } from "../button/button.js";
19
19
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
20
20
  var CAROUSEL_BREAKPOINTS = [
@@ -92,9 +92,10 @@ function Carousel({
92
92
  label,
93
93
  className,
94
94
  slideClassName,
95
- arrowClassName
95
+ arrowClassName,
96
+ motion
96
97
  }) {
97
- const { enabled } = useMotion();
98
+ const { enabled, attribute: motionAttr } = useMotionLevel(motion);
98
99
  const slides = Children.toArray(children);
99
100
  const count = slides.length;
100
101
  const perView = Math.min(
@@ -278,6 +279,7 @@ function Carousel({
278
279
  {
279
280
  "aria-roledescription": "carousel",
280
281
  "aria-label": label ?? "Slides",
282
+ "data-motion": motionAttr,
281
283
  className: cn("relative flex flex-col", className),
282
284
  onMouseEnter: () => setHovered(true),
283
285
  onMouseLeave: () => setHovered(false),
@@ -401,7 +403,7 @@ function Carousel({
401
403
  {
402
404
  "aria-hidden": true,
403
405
  className: cn(
404
- "h-1.5 rounded-full motion-interactive",
406
+ "h-1.5 rounded-full motion-glide",
405
407
  i === logical ? "w-4 bg-foreground" : "w-1.5 bg-muted-foreground/40 group-hover/dot:bg-muted-foreground/70"
406
408
  )
407
409
  }
@@ -60,6 +60,12 @@ var carouselMeta = {
60
60
  name: "arrowClassName",
61
61
  type: "string",
62
62
  description: "Class for both prev/next arrow buttons. Merged through cn(), so a radius or shadow utility replaces the default rounded-full shadow-md rather than stacking with it."
63
+ },
64
+ {
65
+ name: "motion",
66
+ type: '"full" | "subtle" | "off"',
67
+ default: '"full"',
68
+ description: "How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the glide, `off` stills it. It can only turn motion down \u2014 it never overrides a reader's reduce-motion setting. Under subtle the slides still travel and the active dot snaps; off also cuts between slides and pauses autoplay."
63
69
  }
64
70
  ],
65
71
  tokensUsed: [
@@ -70,6 +76,38 @@ var carouselMeta = {
70
76
  "foreground",
71
77
  "muted-foreground"
72
78
  ],
79
+ motion: [
80
+ {
81
+ slot: "arrival",
82
+ recipe: "none",
83
+ part: "Root",
84
+ why: "A carousel arrives with its section. Its content already moves on every advance; an entrance on top would make the first view feel like an advance nobody asked for."
85
+ },
86
+ {
87
+ slot: "response",
88
+ recipe: "motion-interactive",
89
+ part: "Dot",
90
+ why: "The dot's 24px hit area eases its focus ring and the dot brightens under the pointer, which is how a 6px mark tells you it is a button."
91
+ },
92
+ {
93
+ slot: "transition",
94
+ recipe: "custom",
95
+ part: "Track",
96
+ why: `The slide track's translate is set inline from the measured slide pitch, on --motion-duration-slow and the emphasized easing, and switched off during a drag so the track follows the finger. A recipe cannot carry a measured, drag-interrupted offset. Essential: it is the answer to "next", so it plays under subtle.`
97
+ },
98
+ {
99
+ slot: "transition",
100
+ recipe: "motion-glide",
101
+ part: "Dot",
102
+ why: 'The active dot stretches to a bar while the one it left shrinks, on one timeline, so the row keeps its length and the mark reads as travelling with the slide. It used to change width unanimated and jumped while the slides beside it glided. Expressive: motion="subtle" snaps it.'
103
+ },
104
+ {
105
+ slot: "activity",
106
+ recipe: "none",
107
+ part: "Root",
108
+ why: "Autoplay's progress is the slides themselves advancing and the play/pause control; a separate timer animation would put a second clock beside the first."
109
+ }
110
+ ],
73
111
  a11y: [
74
112
  "Region carries aria-roledescription='carousel'; each slide is a group with aria-roledescription='slide' and an 'N of M' label; clones are aria-hidden.",
75
113
  "Arrows and dots are real buttons (dots set aria-current); Left/Right arrow keys navigate when the viewport is focused.",
@@ -92,6 +130,10 @@ var carouselMeta = {
92
130
  {
93
131
  label: "Autoplay",
94
132
  code: "<Carousel loop autoplay={4000} counter>{slides}</Carousel>"
133
+ },
134
+ {
135
+ label: "Subtle motion",
136
+ code: '<Carousel loop motion="subtle" label="Gallery">{slides}</Carousel>'
95
137
  }
96
138
  ]
97
139
  };
@@ -6,9 +6,11 @@ import { Icon } from "../../icon/index.js";
6
6
  import { cn } from "../../lib/cn.js";
7
7
  import { useTypeTier } from "../../lib/density.js";
8
8
  import { setRef } from "../../lib/refs.js";
9
+ import { motionAttribute } from "../../motion/MotionProvider.js";
9
10
  import { Button } from "../button/index.js";
10
11
  import { Textarea } from "../textarea/index.js";
11
12
  import { chatComposerVariants } from "./chat-composer.variants.js";
13
+ import { useEasedGrowth } from "./use-eased-growth.js";
12
14
  import { jsx, jsxs } from "react/jsx-runtime";
13
15
  function ChatComposer({
14
16
  value,
@@ -31,6 +33,7 @@ function ChatComposer({
31
33
  stopLabel = "Stop generating",
32
34
  footer,
33
35
  fieldRef,
36
+ motion,
34
37
  className,
35
38
  children,
36
39
  ...props
@@ -82,9 +85,11 @@ function ChatComposer({
82
85
  [send, mode]
83
86
  );
84
87
  const showStop = busy && Boolean(onStop);
88
+ useEasedGrowth(field, text);
85
89
  return /* @__PURE__ */ jsxs(
86
90
  "form",
87
91
  {
92
+ "data-motion": motionAttribute(motion),
88
93
  onSubmit: handleSubmit,
89
94
  className: cn(chatComposerVariants({ invalid }), className),
90
95
  ...props,
@@ -120,6 +125,9 @@ function ChatComposer({
120
125
  // Inline padding puts the text on the attach glyph's edge: an
121
126
  // icon-sm button centres a 1.125rem glyph in --control-sm.
122
127
  "min-h-0 max-h-40 resize-none border-0 bg-transparent px-[calc((var(--control-sm)-1.125rem)/2)] py-1 focus-visible:ring-0",
128
+ // Past the cap the field scrolls; the platform bar (stepper arrows on
129
+ // Windows) would poke into the composer's rounded corner.
130
+ "scrollbar-quiet",
123
131
  // What you type has to match what it becomes: the field and the
124
132
  // Bubble it turns into both read `prose`, so the message does not
125
133
  // change size the moment it is sent.
@@ -143,33 +151,25 @@ function ChatComposer({
143
151
  leading,
144
152
  /* @__PURE__ */ jsx("div", { className: "flex-1" }),
145
153
  actions,
146
- showStop ? /* @__PURE__ */ jsx(
154
+ /* @__PURE__ */ jsx(
147
155
  Button,
148
156
  {
149
- type: "button",
150
- variant: "secondary",
157
+ type: showStop ? "button" : "submit",
158
+ variant: showStop ? "secondary" : "primary",
151
159
  size: "icon-sm",
152
- "aria-label": stopLabel,
153
- onClick: () => {
160
+ "aria-label": showStop ? stopLabel : sendLabel,
161
+ disabled: showStop ? false : !canSend,
162
+ loading: !showStop && busy,
163
+ onClick: showStop ? (event) => {
164
+ event.preventDefault();
154
165
  onStop?.();
155
166
  if (!coarse) field.current?.focus();
156
- },
157
- children: /* @__PURE__ */ jsx(Icon, { name: "stop", variant: "solid", className: "text-[0.75em]" })
167
+ } : void 0,
168
+ children: /* @__PURE__ */ jsxs("span", { "aria-hidden": "true", className: "motion-icon-swap", children: [
169
+ /* @__PURE__ */ jsx("span", { "data-active": !showStop, className: "inline-flex", children: /* @__PURE__ */ jsx(Icon, { name: "arrow-up" }) }),
170
+ /* @__PURE__ */ jsx("span", { "data-active": showStop, className: "inline-flex", children: /* @__PURE__ */ jsx(Icon, { name: "stop", variant: "solid", className: "text-[0.75em]" }) })
171
+ ] })
158
172
  }
159
- ) : (
160
- // Busy without a stop handler: Button's own `loading` swaps the glyph
161
- // for its Spinner and keeps full colour, rather than a hand-spun icon.
162
- /* @__PURE__ */ jsx(
163
- Button,
164
- {
165
- type: "submit",
166
- size: "icon-sm",
167
- "aria-label": sendLabel,
168
- disabled: !canSend,
169
- loading: busy,
170
- children: /* @__PURE__ */ jsx(Icon, { name: "arrow-up" })
171
- }
172
- )
173
173
  )
174
174
  ] }),
175
175
  footerContent ? /* @__PURE__ */ jsx("p", { className: cn("px-[calc((var(--control-sm)-1.125rem)/2)] text-muted-foreground", tier.secondary), children: footerContent }) : null,
@@ -209,6 +209,12 @@ var chatComposerMeta = {
209
209
  type: "Ref<HTMLTextAreaElement>",
210
210
  description: "Ref to the underlying textarea \u2014 for focusing after a send, or on mount, and the extension point for anything that listens to the field. Pass `useCaretTrigger`'s `fieldRef` here for mentions and slash commands (CaretMenu). An Enter another listener already handled (`preventDefault()`) does not send."
211
211
  },
212
+ {
213
+ name: "motion",
214
+ type: '"full" | "subtle" | "off"',
215
+ default: '"full"',
216
+ description: "How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the flourish, `off` stills it. It can only turn motion down \u2014 it never overrides a reader's reduce-motion setting. Under `subtle` the field snaps to its new height and the send/stop glyphs swap without the morph."
217
+ },
212
218
  {
213
219
  name: "...form props",
214
220
  type: 'Omit<ComponentPropsWithRef<"form">, "onSubmit" | "defaultValue">',
@@ -230,6 +236,38 @@ var chatComposerMeta = {
230
236
  "accent",
231
237
  "accent-foreground"
232
238
  ],
239
+ motion: [
240
+ {
241
+ slot: "arrival",
242
+ recipe: "none",
243
+ part: "Root",
244
+ why: "The composer is the fixed foot of a thread, there before anything else is. An entrance would only delay the first keystroke."
245
+ },
246
+ {
247
+ slot: "response",
248
+ recipe: "motion-interactive",
249
+ part: "Root",
250
+ why: "The surface's focus ring and invalid border ease in, so focusing the field reads as the whole box waking up rather than a frame flicker."
251
+ },
252
+ {
253
+ slot: "transition",
254
+ recipe: "custom",
255
+ part: "Field",
256
+ why: "As a line breaks or a paste lands, the field eases to its new height (up to its cap) instead of jumping, so the transcript above is visibly pushed rather than teleported. `field-sizing` alone cannot ease \u2014 its height stays `auto` \u2014 so a small hook measures and pins a pixel height. Expressive and short (`fast`): the text itself lands at once, and `subtle` snaps the box."
257
+ },
258
+ {
259
+ slot: "transition",
260
+ recipe: "motion-icon-swap",
261
+ part: "Send button",
262
+ why: "Send turning into stop is the moment a reply starts, and stop turning back is the moment it is over. One button changes role in place \u2014 the fill eases, the arrow collapses into the square \u2014 so it reads as the same control changing what it does, not a different button appearing under the cursor."
263
+ },
264
+ {
265
+ slot: "activity",
266
+ recipe: "motion-spin",
267
+ part: "Send button",
268
+ why: "Busy with no stop handler, Button's own loading Spinner replaces the arrow, so a send in flight is not mistaken for a disabled one."
269
+ }
270
+ ],
233
271
  a11y: [
234
272
  "A real `<form>`, so the send button is a submit button and the whole thing behaves as one control to assistive tech and to password managers alike.",
235
273
  "Enter never submits while an IME composition is open (`isComposing`), so a CJK reader confirming a candidate word does not mail a fragment of their first sentence.",
@@ -0,0 +1,70 @@
1
+ "use client";
2
+
3
+ // src/components/chat-composer/use-eased-growth.ts
4
+ import { useCallback, useEffect, useRef } from "react";
5
+ import { useIsomorphicLayoutEffect } from "../../lib/use-isomorphic-layout-effect.js";
6
+ var GROW_TRANSITION = "height calc(var(--motion-duration-fast) * var(--motion-expressive, 1)) var(--motion-ease-standard), opacity var(--motion-duration-fast) var(--motion-ease-standard)";
7
+ function useEasedGrowth(ref, value) {
8
+ const mounted = useRef(false);
9
+ const width = useRef(0);
10
+ const fit = useCallback(
11
+ (animate) => {
12
+ const el = ref.current;
13
+ if (!el) return;
14
+ const style = getComputedStyle(el);
15
+ const from = Number.parseFloat(style.height);
16
+ const scrollTop = el.scrollTop;
17
+ el.style.transition = "none";
18
+ el.style.height = "auto";
19
+ if (el.scrollHeight === 0) {
20
+ el.style.height = "";
21
+ el.style.overflowY = "";
22
+ el.style.transition = GROW_TRANSITION;
23
+ return;
24
+ }
25
+ const content = el.scrollHeight + (Number.parseFloat(style.borderTopWidth) || 0) + (Number.parseFloat(style.borderBottomWidth) || 0);
26
+ const max = Number.parseFloat(style.maxHeight);
27
+ const target = Number.isNaN(max) ? content : Math.min(content, max);
28
+ const capped = content > target + 0.5;
29
+ el.style.overflowY = capped ? "" : "hidden";
30
+ if (animate && Number.isFinite(from)) {
31
+ el.style.height = `${from}px`;
32
+ void el.offsetHeight;
33
+ el.style.transition = GROW_TRANSITION;
34
+ el.style.height = `${target}px`;
35
+ } else {
36
+ el.style.height = `${target}px`;
37
+ void el.offsetHeight;
38
+ el.style.transition = GROW_TRANSITION;
39
+ }
40
+ el.scrollTop = capped ? scrollTop : 0;
41
+ },
42
+ [ref]
43
+ );
44
+ useIsomorphicLayoutEffect(() => {
45
+ fit(mounted.current);
46
+ mounted.current = true;
47
+ }, [value, fit]);
48
+ useEffect(() => {
49
+ const el = ref.current;
50
+ if (!el || typeof ResizeObserver === "undefined") return;
51
+ width.current = el.clientWidth;
52
+ const ro = new ResizeObserver(() => {
53
+ if (el.clientWidth === width.current) return;
54
+ width.current = el.clientWidth;
55
+ fit(false);
56
+ });
57
+ ro.observe(el);
58
+ let live = true;
59
+ document.fonts?.ready.then(() => {
60
+ if (live) fit(false);
61
+ });
62
+ return () => {
63
+ live = false;
64
+ ro.disconnect();
65
+ };
66
+ }, [ref, fit]);
67
+ }
68
+ export {
69
+ useEasedGrowth
70
+ };
@@ -9,7 +9,14 @@ function CheckboxMark({ className }) {
9
9
  Base.Indicator,
10
10
  {
11
11
  className: cn(
12
- "flex items-center justify-center text-current motion-interactive ease-spring data-[starting-style]:scale-0 data-[starting-style]:opacity-0 data-[ending-style]:scale-0 data-[ending-style]:opacity-0",
12
+ // motion-tick, not a scale from 0: the mark wipes in from its leading
13
+ // stroke, so checking reads as a tick being drawn rather than a dot
14
+ // swelling into a check. The dash takes the same wipe, which for a
15
+ // horizontal bar is simply a line drawn left to right. It declares its
16
+ // own transition list, so motion-interactive must not sit here too —
17
+ // two transition-property declarations on one element and the later
18
+ // class silently wins, dropping the clip-path.
19
+ "flex items-center justify-center text-current motion-tick",
13
20
  className
14
21
  ),
15
22
  render: (indicatorProps, state) => /* @__PURE__ */ jsx("span", { ...indicatorProps, children: /* @__PURE__ */ jsx(
@@ -1,13 +1,15 @@
1
1
  // src/components/checkbox/checkbox.tsx
2
2
  import { Checkbox as Base } from "@base-ui/react/checkbox";
3
3
  import { cn } from "../../lib/cn.js";
4
+ import { motionAttribute } from "../../motion/MotionProvider.js";
4
5
  import { CHECK_BOX } from "../../lib/selection-card.js";
5
6
  import { CheckboxMark } from "./checkbox-mark.js";
6
7
  import { jsx } from "react/jsx-runtime";
7
- function Checkbox({ className, children, ...props }) {
8
+ function Checkbox({ className, children, motion, ...props }) {
8
9
  return /* @__PURE__ */ jsx(
9
10
  Base.Root,
10
11
  {
12
+ "data-motion": motionAttribute(motion),
11
13
  className: cn(
12
14
  CHECK_BOX,
13
15
  "text-primary-foreground",
@@ -91,6 +91,12 @@ var checkboxMeta = {
91
91
  name: "render",
92
92
  type: "ReactElement | (props, state) => ReactElement",
93
93
  description: "Compose the root with another element through the Base UI render prop."
94
+ },
95
+ {
96
+ name: "motion",
97
+ type: '"full" | "subtle" | "off"',
98
+ default: '"full"',
99
+ description: "How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the flourish, `off` stills it. It can only turn motion down \u2014 it never overrides a reader's reduce-motion setting. Under `subtle` the tick fades in instead of drawing itself."
94
100
  }
95
101
  ],
96
102
  tokensUsed: [
@@ -100,6 +106,32 @@ var checkboxMeta = {
100
106
  "primary-foreground",
101
107
  "ring"
102
108
  ],
109
+ motion: [
110
+ {
111
+ slot: "arrival",
112
+ recipe: "none",
113
+ part: "Root",
114
+ why: "It arrives with the form it belongs to. Animating each box in turns a settings list into a title sequence."
115
+ },
116
+ {
117
+ slot: "response",
118
+ recipe: "motion-interactive",
119
+ part: "Box",
120
+ why: "The box fills with the primary colour as it is checked; easing the fill keeps the mark and the box changing as one event rather than two frames apart."
121
+ },
122
+ {
123
+ slot: "transition",
124
+ recipe: "motion-tick",
125
+ part: "Mark",
126
+ why: "The check draws itself in from its leading stroke, so checking reads as the box being ticked rather than a glyph appearing. The dash wipes the same way. Unchecking only fades, so clearing a list is quick; under `subtle` checking fades too."
127
+ },
128
+ {
129
+ slot: "activity",
130
+ recipe: "none",
131
+ part: "Root",
132
+ why: "A checkbox commits instantly. A slow write belongs to the surrounding form's pending state, not the control."
133
+ }
134
+ ],
103
135
  a11y: [
104
136
  "Base UI renders a hidden native <input type=checkbox> for keyboard, focus, and form semantics.",
105
137
  "Space toggles; focus-visible draws a 2px ring in the --ring token.",
@@ -2,6 +2,7 @@
2
2
  import { Checkbox as Base } from "@base-ui/react/checkbox";
3
3
  import { CheckboxGroup as BaseGroup } from "@base-ui/react/checkbox-group";
4
4
  import { cn } from "../../lib/cn.js";
5
+ import { motionAttribute } from "../../motion/MotionProvider.js";
5
6
  import {
6
7
  CHECK_BOX,
7
8
  SELECTION_CARD_GLYPH,
@@ -52,6 +53,7 @@ function CheckboxCard({
52
53
  description,
53
54
  icon,
54
55
  tone = "neutral",
56
+ motion,
55
57
  ...props
56
58
  }) {
57
59
  const t = checkboxCardToneClasses[tone];
@@ -59,6 +61,7 @@ function CheckboxCard({
59
61
  return /* @__PURE__ */ jsxs(
60
62
  Base.Root,
61
63
  {
64
+ "data-motion": motionAttribute(motion),
62
65
  className: cn(
63
66
  SELECTION_CARD_SHELL,
64
67
  icon != null ? SELECTION_CARD_LAYOUT.tile : SELECTION_CARD_LAYOUT.row,
@@ -92,6 +92,12 @@ var checkboxCardMeta = {
92
92
  name: "checked / onCheckedChange / defaultChecked / disabled",
93
93
  type: "Base UI Checkbox.Root props",
94
94
  description: "Standard checkbox state. Without a group-owned value, each card is independent \u2014 drive checked/onCheckedChange per item for multi-select."
95
+ },
96
+ {
97
+ name: "motion",
98
+ type: '"full" | "subtle" | "off"',
99
+ default: '"full"',
100
+ description: "How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the flourish, `off` stills it. It can only turn motion down \u2014 it never overrides a reader's reduce-motion setting. Under `subtle` the tick fades in instead of drawing itself."
95
101
  }
96
102
  ],
97
103
  subcomponents: [
@@ -124,6 +130,32 @@ var checkboxCardMeta = {
124
130
  "tone-warning",
125
131
  "tone-warning-foreground"
126
132
  ],
133
+ motion: [
134
+ {
135
+ slot: "arrival",
136
+ recipe: "none",
137
+ part: "Root",
138
+ why: "It arrives with the form it belongs to. Animating each box in turns a settings list into a title sequence."
139
+ },
140
+ {
141
+ slot: "response",
142
+ recipe: "motion-interactive",
143
+ part: "Box",
144
+ why: "The box fills with the primary colour as it is checked; easing the fill keeps the mark and the box changing as one event rather than two frames apart."
145
+ },
146
+ {
147
+ slot: "transition",
148
+ recipe: "motion-tick",
149
+ part: "Mark",
150
+ why: "The check draws itself in from its leading stroke, so checking reads as the box being ticked rather than a glyph appearing. The dash wipes the same way. Unchecking only fades, so clearing a list is quick; under `subtle` checking fades too."
151
+ },
152
+ {
153
+ slot: "activity",
154
+ recipe: "none",
155
+ part: "Root",
156
+ why: "A checkbox commits instantly. A slow write belongs to the surrounding form's pending state, not the control."
157
+ }
158
+ ],
127
159
  a11y: [
128
160
  "Base UI provides the checkbox role, aria-checked, hidden native input, keyboard toggle, and data-[checked]/data-disabled state.",
129
161
  "The whole card is the hit target; focus draws a 2px outline outside the card (not a ring, so the checked tone ring survives focus); Space toggles the focused card.",
@@ -7,6 +7,15 @@ import { Button } from "../button/button.js";
7
7
  import { cn } from "../../lib/cn.js";
8
8
  import { jsx, jsxs } from "react/jsx-runtime";
9
9
  var CodeHighlightContext = createContext(null);
10
+ function CopyGlyph({ copied }) {
11
+ return /* @__PURE__ */ jsxs("span", { "aria-hidden": "true", className: "motion-icon-swap", children: [
12
+ /* @__PURE__ */ jsx("span", { "data-active": !copied, className: "inline-flex", children: /* @__PURE__ */ jsx(Icon, { name: "copy" }) }),
13
+ /* @__PURE__ */ jsx("span", { "data-active": copied, className: "inline-flex", children: /* @__PURE__ */ jsx(Icon, { name: "check" }) })
14
+ ] });
15
+ }
16
+ function CopiedStatus({ copied }) {
17
+ return /* @__PURE__ */ jsx("span", { role: "status", className: "sr-only", children: copied ? "Copied" : "" });
18
+ }
10
19
  function CodeHighlightProvider({ highlight, children }) {
11
20
  return /* @__PURE__ */ jsx(CodeHighlightContext.Provider, { value: highlight, children });
12
21
  }
@@ -39,7 +48,8 @@ function CodeBlock({
39
48
  ] }),
40
49
  code
41
50
  ] }),
42
- showCopy ? /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Copy command", onClick: copy, className: "shrink-0 text-muted-foreground", children: /* @__PURE__ */ jsx(Icon, { name: copied ? "check" : "copy" }) }) : null
51
+ showCopy ? /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Copy command", onClick: copy, className: "shrink-0 text-muted-foreground", children: /* @__PURE__ */ jsx(CopyGlyph, { copied }) }) : null,
52
+ showCopy ? /* @__PURE__ */ jsx(CopiedStatus, { copied }) : null
43
53
  ]
44
54
  }
45
55
  );
@@ -57,7 +67,7 @@ function CodeBlock({
57
67
  children: [
58
68
  hasHeader ? /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 border-b border-border px-4 py-2", children: [
59
69
  /* @__PURE__ */ jsx("span", { className: "text-mono-caption text-muted-foreground", children: filename }),
60
- showCopy ? /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Copy code", onClick: copy, className: "text-muted-foreground", children: /* @__PURE__ */ jsx(Icon, { name: copied ? "check" : "copy" }) }) : null
70
+ showCopy ? /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Copy code", onClick: copy, className: "text-muted-foreground", children: /* @__PURE__ */ jsx(CopyGlyph, { copied }) }) : null
61
71
  ] }) : null,
62
72
  /* @__PURE__ */ jsx(
63
73
  "pre",
@@ -72,7 +82,8 @@ function CodeBlock({
72
82
  children: /* @__PURE__ */ jsx("code", { children: highlighter ? highlighter(code) : code })
73
83
  }
74
84
  ),
75
- copyColumn ? /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Copy code", onClick: copy, className: "m-2 ms-0 shrink-0 text-muted-foreground", children: /* @__PURE__ */ jsx(Icon, { name: copied ? "check" : "copy" }) }) : null
85
+ copyColumn ? /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Copy code", onClick: copy, className: "m-2 ms-0 shrink-0 text-muted-foreground", children: /* @__PURE__ */ jsx(CopyGlyph, { copied }) }) : null,
86
+ showCopy ? /* @__PURE__ */ jsx(CopiedStatus, { copied }) : null
76
87
  ]
77
88
  }
78
89
  );
@@ -79,9 +79,36 @@ var codeBlockMeta = {
79
79
  }
80
80
  ],
81
81
  tokensUsed: ["border", "muted", "muted-foreground", "foreground"],
82
+ motion: [
83
+ {
84
+ slot: "arrival",
85
+ recipe: "none",
86
+ part: "Root",
87
+ why: "Code is reading content and arrives with the page or message it belongs to. An entrance of its own would delay the text someone came to copy."
88
+ },
89
+ {
90
+ slot: "response",
91
+ recipe: "motion-interactive",
92
+ part: "Copy button",
93
+ why: "The copy button is a quiet ghost icon in the corner of a block. Its hover fill is what tells the pointer it is a control and not decoration."
94
+ },
95
+ {
96
+ slot: "transition",
97
+ recipe: "motion-icon-swap",
98
+ part: "Copy glyph",
99
+ why: "The copy mark turning into a check is the only proof the clipboard took the text. Snapped between frames it was easy to miss at the edge of a block; the swap reads as one mark being replaced."
100
+ },
101
+ {
102
+ slot: "activity",
103
+ recipe: "none",
104
+ part: "Root",
105
+ why: "Copying is instant. There is no in-between worth showing, and a spinner for a clipboard write would read as a slow one."
106
+ }
107
+ ],
82
108
  a11y: [
83
109
  "Code renders inside a <pre><code> for correct semantics and screen-reader handling.",
84
- "Copy is the ghost icon Button with `useCopy`, which guards clipboard access for SSR safety."
110
+ "Copy is the ghost icon Button with `useCopy`, which guards clipboard access for SSR safety.",
111
+ 'A successful copy is announced as "Copied" through a polite role=status region that is always mounted; the button keeps its label, which names what it does. Both glyphs are aria-hidden.'
85
112
  ],
86
113
  examples: [
87
114
  {