@sableui/react 0.13.1 → 0.14.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 (231) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +132 -0
  3. package/CONTRIBUTING.md +7 -5
  4. package/DESIGN.md +8 -3
  5. package/bin/sable.mjs +17 -1
  6. package/dist/blocks/approval-block.js +93 -0
  7. package/dist/blocks/approval-block.meta.js +23 -0
  8. package/dist/blocks/index.d.ts +17 -2
  9. package/dist/blocks/index.js +4 -0
  10. package/dist/components/accordion/accordion.meta.js +10 -3
  11. package/dist/components/area-chart/area-chart.meta.js +10 -3
  12. package/dist/components/attachment/attachment.meta.js +26 -0
  13. package/dist/components/badge/badge.meta.js +2 -1
  14. package/dist/components/banner/banner.meta.js +4 -2
  15. package/dist/components/bar-chart/bar-chart.meta.js +11 -4
  16. package/dist/components/bottom-nav/bottom-nav.js +3 -1
  17. package/dist/components/bottom-nav/bottom-nav.meta.js +32 -0
  18. package/dist/components/breadcrumbs/breadcrumbs.meta.js +26 -0
  19. package/dist/components/button/button.js +21 -4
  20. package/dist/components/button/button.meta.js +7 -7
  21. package/dist/components/button-group/button-group.meta.js +2 -1
  22. package/dist/components/calendar/calendar.js +18 -1
  23. package/dist/components/calendar/calendar.meta.js +38 -0
  24. package/dist/components/carousel/carousel.js +6 -4
  25. package/dist/components/carousel/carousel.meta.js +42 -0
  26. package/dist/components/chat-composer/chat-composer.js +21 -21
  27. package/dist/components/chat-composer/chat-composer.meta.js +38 -0
  28. package/dist/components/chat-composer/use-eased-growth.js +70 -0
  29. package/dist/components/checkbox/checkbox-mark.js +8 -1
  30. package/dist/components/checkbox/checkbox.js +3 -1
  31. package/dist/components/checkbox/checkbox.meta.js +34 -1
  32. package/dist/components/checkbox-card/checkbox-card.js +3 -0
  33. package/dist/components/checkbox-card/checkbox-card.meta.js +32 -0
  34. package/dist/components/code-block/code-block.js +14 -3
  35. package/dist/components/code-block/code-block.meta.js +28 -1
  36. package/dist/components/collapsible/collapsible.meta.js +34 -1
  37. package/dist/components/color-field/color-field.meta.js +26 -0
  38. package/dist/components/combobox/combobox.meta.js +48 -2
  39. package/dist/components/command-menu/command-menu.js +8 -3
  40. package/dist/components/command-menu/command-menu.meta.js +44 -3
  41. package/dist/components/confirm-button/confirm-button.js +179 -0
  42. package/dist/components/confirm-button/confirm-button.meta.js +143 -0
  43. package/dist/components/confirm-button/index.js +7 -0
  44. package/dist/components/context-card/context-card.meta.js +2 -1
  45. package/dist/components/data-table/data-table.js +6 -2
  46. package/dist/components/data-table/data-table.meta.js +32 -0
  47. package/dist/components/dialog/dialog.meta.js +6 -3
  48. package/dist/components/disclosure-picker/disclosure-picker.meta.js +2 -1
  49. package/dist/components/entity/entity.meta.js +26 -0
  50. package/dist/components/feedback/feedback.js +113 -47
  51. package/dist/components/feedback/feedback.meta.js +49 -4
  52. package/dist/components/inline-status/inline-status.meta.js +2 -1
  53. package/dist/components/input-group/input-group.meta.js +27 -1
  54. package/dist/components/line-chart/line-chart.meta.js +10 -3
  55. package/dist/components/menu/menu.meta.js +2 -1
  56. package/dist/components/menubar/menubar.meta.js +2 -1
  57. package/dist/components/meter/meter.meta.js +2 -1
  58. package/dist/components/middle-truncate/middle-truncate.meta.js +1 -1
  59. package/dist/components/note/note.meta.js +34 -1
  60. package/dist/components/notification-badge/notification-badge.js +17 -5
  61. package/dist/components/notification-badge/notification-badge.meta.js +34 -2
  62. package/dist/components/number-ticker/index.js +7 -0
  63. package/dist/components/number-ticker/number-ticker.js +246 -0
  64. package/dist/components/number-ticker/number-ticker.meta.js +159 -0
  65. package/dist/components/otp-field/otp-field.js +23 -3
  66. package/dist/components/otp-field/otp-field.meta.js +23 -2
  67. package/dist/components/page-loader/page-loader.meta.js +2 -1
  68. package/dist/components/panel-group/panel-group.meta.js +26 -0
  69. package/dist/components/panel-section/panel-section.js +37 -10
  70. package/dist/components/panel-section/panel-section.meta.js +32 -0
  71. package/dist/components/pie-chart/pie-chart.js +20 -11
  72. package/dist/components/pie-chart/pie-chart.meta.js +17 -4
  73. package/dist/components/pill/pill.meta.js +34 -1
  74. package/dist/components/popover/popover.meta.js +4 -2
  75. package/dist/components/progress/progress.meta.js +2 -1
  76. package/dist/components/radar-chart/radar-chart.js +4 -3
  77. package/dist/components/radar-chart/radar-chart.meta.js +10 -3
  78. package/dist/components/radial-chart/radial-chart.js +22 -22
  79. package/dist/components/radial-chart/radial-chart.meta.js +17 -4
  80. package/dist/components/radio/radio.meta.js +28 -1
  81. package/dist/components/radio-card/radio-card.meta.js +38 -0
  82. package/dist/components/relative-time/relative-time.meta.js +32 -0
  83. package/dist/components/scroller/scroller.meta.js +32 -0
  84. package/dist/components/scrub-field/scrub-field.meta.js +32 -0
  85. package/dist/components/search-input/search-input.js +9 -7
  86. package/dist/components/search-input/search-input.meta.js +26 -0
  87. package/dist/components/segmented-bar/segmented-bar.meta.js +2 -1
  88. package/dist/components/select/select.meta.js +4 -2
  89. package/dist/components/sheet/sheet.meta.js +4 -2
  90. package/dist/components/show-more/show-more.js +1 -4
  91. package/dist/components/show-more/show-more.meta.js +38 -0
  92. package/dist/components/side-nav/side-nav.js +21 -1
  93. package/dist/components/side-nav/side-nav.meta.js +34 -1
  94. package/dist/components/side-nav/side-nav.variants.js +3 -1
  95. package/dist/components/sidebar/sidebar.js +31 -8
  96. package/dist/components/sidebar/sidebar.meta.js +38 -0
  97. package/dist/components/skeleton/skeleton.meta.js +2 -1
  98. package/dist/components/slider/slider.meta.js +26 -0
  99. package/dist/components/sparkline/sparkline.js +7 -4
  100. package/dist/components/sparkline/sparkline.meta.js +9 -2
  101. package/dist/components/spinner/spinner.meta.js +2 -1
  102. package/dist/components/stepper/stepper.meta.js +4 -2
  103. package/dist/components/switch/switch.meta.js +2 -1
  104. package/dist/components/table/table.meta.js +28 -1
  105. package/dist/components/tabs/tabs.js +27 -8
  106. package/dist/components/tabs/tabs.meta.js +50 -5
  107. package/dist/components/theme-selector/theme-selector.meta.js +32 -0
  108. package/dist/components/theme-switcher/theme-switcher.js +4 -1
  109. package/dist/components/theme-switcher/theme-switcher.meta.js +42 -0
  110. package/dist/components/thinking-orb/thinking-orb.meta.js +4 -2
  111. package/dist/components/toast/toast.meta.js +2 -1
  112. package/dist/components/toggle/toggle.js +28 -5
  113. package/dist/components/toggle/toggle.meta.js +46 -3
  114. package/dist/components/tool-call/index.js +8 -0
  115. package/dist/components/tool-call/tool-call.js +117 -0
  116. package/dist/components/tool-call/tool-call.meta.js +147 -0
  117. package/dist/components/tool-call/tool-call.variants.js +33 -0
  118. package/dist/components/toolbar/toolbar.meta.js +2 -1
  119. package/dist/components/tooltip/index.js +3 -1
  120. package/dist/components/tooltip/tooltip.js +84 -0
  121. package/dist/components/tooltip/tooltip.meta.js +18 -2
  122. package/dist/components/tree/tree.js +71 -6
  123. package/dist/components/tree/tree.meta.js +32 -0
  124. package/dist/components/video/video.meta.js +32 -0
  125. package/dist/file-hashes.json +139 -122
  126. package/dist/foundations/motion.js +49 -3
  127. package/dist/index.d.ts +616 -46
  128. package/dist/index.js +4 -1
  129. package/dist/lib/chart/cartesian.js +9 -10
  130. package/dist/lib/chart/chart-center.js +30 -0
  131. package/dist/lib/chart/chart-frame.js +15 -8
  132. package/dist/lib/chart/dim.js +13 -0
  133. package/dist/lib/chart/format.js +7 -1
  134. package/dist/lib/chart/motion.js +6 -4
  135. package/dist/lib/highlight-glide.js +68 -10
  136. package/dist/lib/taxonomy.js +3 -0
  137. package/dist/{meta-DVG8ceze.d.ts → meta-DX5r5wQL.d.ts} +8 -0
  138. package/dist/motion/MotionProvider.js +45 -3
  139. package/dist/motion/Reveal.js +1 -1
  140. package/dist/motion/TextReveal.js +1 -1
  141. package/dist/motion/index.js +15 -2
  142. package/dist/motion/useAutoSize.js +42 -0
  143. package/dist/motion/useShake.js +25 -0
  144. package/dist/patterns/index.d.ts +1 -1
  145. package/dist/styles.css +298 -5
  146. package/llms/blocks/approval-block.md +128 -0
  147. package/llms/components/accordion.md +4 -3
  148. package/llms/components/area-chart.md +4 -3
  149. package/llms/components/attachment.md +7 -0
  150. package/llms/components/badge.md +1 -1
  151. package/llms/components/banner.md +2 -2
  152. package/llms/components/bar-chart.md +5 -4
  153. package/llms/components/bottom-nav.md +8 -0
  154. package/llms/components/breadcrumbs.md +7 -0
  155. package/llms/components/button-group.md +1 -1
  156. package/llms/components/button.md +6 -6
  157. package/llms/components/calendar.md +9 -0
  158. package/llms/components/carousel.md +17 -0
  159. package/llms/components/chat-composer.md +9 -0
  160. package/llms/components/checkbox-card.md +8 -0
  161. package/llms/components/checkbox.md +9 -1
  162. package/llms/components/code-block.md +8 -0
  163. package/llms/components/collapsible.md +9 -1
  164. package/llms/components/color-field.md +7 -0
  165. package/llms/components/combobox.md +12 -2
  166. package/llms/components/command-menu.md +12 -3
  167. package/llms/components/confirm-button.md +92 -0
  168. package/llms/components/context-card.md +1 -1
  169. package/llms/components/data-table.md +8 -0
  170. package/llms/components/dialog.md +3 -3
  171. package/llms/components/disclosure-picker.md +1 -1
  172. package/llms/components/entity.md +7 -0
  173. package/llms/components/feedback.md +22 -3
  174. package/llms/components/inline-status.md +1 -1
  175. package/llms/components/input-group.md +8 -1
  176. package/llms/components/line-chart.md +4 -3
  177. package/llms/components/menu.md +1 -1
  178. package/llms/components/menubar.md +1 -1
  179. package/llms/components/meter.md +1 -1
  180. package/llms/components/middle-truncate.md +1 -1
  181. package/llms/components/note.md +9 -1
  182. package/llms/components/notification-badge.md +10 -2
  183. package/llms/components/number-ticker.md +86 -0
  184. package/llms/components/otp-field.md +15 -1
  185. package/llms/components/page-loader.md +1 -1
  186. package/llms/components/panel-group.md +7 -0
  187. package/llms/components/panel-section.md +8 -0
  188. package/llms/components/pie-chart.md +6 -4
  189. package/llms/components/pill.md +9 -1
  190. package/llms/components/popover.md +2 -2
  191. package/llms/components/progress.md +1 -1
  192. package/llms/components/radar-chart.md +4 -3
  193. package/llms/components/radial-chart.md +6 -4
  194. package/llms/components/radio-card.md +9 -0
  195. package/llms/components/radio.md +8 -1
  196. package/llms/components/relative-time.md +8 -0
  197. package/llms/components/scroller.md +8 -0
  198. package/llms/components/scrub-field.md +8 -0
  199. package/llms/components/search-input.md +7 -0
  200. package/llms/components/segmented-bar.md +1 -1
  201. package/llms/components/select.md +2 -2
  202. package/llms/components/sheet.md +2 -2
  203. package/llms/components/show-more.md +9 -0
  204. package/llms/components/side-nav.md +9 -1
  205. package/llms/components/sidebar.md +9 -0
  206. package/llms/components/skeleton.md +1 -1
  207. package/llms/components/slider.md +7 -0
  208. package/llms/components/sparkline.md +3 -2
  209. package/llms/components/spinner.md +1 -1
  210. package/llms/components/stepper.md +2 -2
  211. package/llms/components/switch.md +1 -1
  212. package/llms/components/table.md +8 -1
  213. package/llms/components/tabs.md +22 -4
  214. package/llms/components/theme-selector.md +8 -0
  215. package/llms/components/theme-switcher.md +17 -0
  216. package/llms/components/thinking-orb.md +2 -2
  217. package/llms/components/toast.md +1 -1
  218. package/llms/components/toggle.md +20 -3
  219. package/llms/components/tool-call.md +115 -0
  220. package/llms/components/toolbar.md +1 -1
  221. package/llms/components/tooltip.md +12 -2
  222. package/llms/components/tree.md +8 -0
  223. package/llms/components/video.md +8 -0
  224. package/llms/foundations/motion.md +48 -3
  225. package/llms/reference/utilities.md +23 -1
  226. package/llms/tasks/reviewing-motion.md +14 -0
  227. package/llms-full.txt +1004 -98
  228. package/llms.txt +16 -11
  229. package/package.json +1 -1
  230. package/registry.json +2708 -187
  231. package/targets/ios.json +27 -0
@@ -66,21 +66,23 @@ function SearchInput({
66
66
  }
67
67
  ),
68
68
  hasValue && !props.disabled ? (
69
- // Sable's own icon button, not a hand-sized <button>: the hit area,
70
- // focus ring, press and disabled fade follow the control ladder with
71
- // every other icon button. Muted ink until hovered, so the X never
72
- // out-shouts the query it clears.
73
- /* @__PURE__ */ jsx(
69
+ // The wrapper carries the arrival, the button keeps its own response.
70
+ // motion-appear declares a transition list of its own, and on the
71
+ // Button it would fight motion-interactive for the one
72
+ // transition-property slot — whichever sorted last would win, and
73
+ // either the hover ease or the arrival would silently go. Removal
74
+ // stays instant: an emptied field should not wait on its X.
75
+ /* @__PURE__ */ jsx("span", { className: "absolute end-2 flex motion-appear", children: /* @__PURE__ */ jsx(
74
76
  Button,
75
77
  {
76
78
  variant: "ghost",
77
79
  size: "icon-xs",
78
80
  onClick: handleClear,
79
81
  "aria-label": "Clear search",
80
- className: "absolute end-2 text-muted-foreground hover:text-foreground",
82
+ className: "text-muted-foreground hover:text-foreground",
81
83
  children: /* @__PURE__ */ jsx(Icon, { name: "xmark" })
82
84
  }
83
- )
85
+ ) })
84
86
  ) : null
85
87
  ] })
86
88
  );
@@ -98,6 +98,32 @@ var searchInputMeta = {
98
98
  "ring",
99
99
  "destructive"
100
100
  ],
101
+ motion: [
102
+ {
103
+ slot: "arrival",
104
+ recipe: "motion-appear",
105
+ part: "Clear button",
106
+ why: "The X mounts as the first character lands. Fading and settling in from a smaller scale keeps it from popping into the corner of the eye on every new search; removal is instant so an emptied field does not wait on it."
107
+ },
108
+ {
109
+ slot: "response",
110
+ recipe: "motion-interactive",
111
+ part: "Input",
112
+ why: "The focus ring and invalid border ease in, so focus reads as the field answering rather than a repaint."
113
+ },
114
+ {
115
+ slot: "transition",
116
+ recipe: "none",
117
+ part: "Root",
118
+ why: "The field has no states of its own to move between \u2014 the clear button arriving is its only change, and arrival covers it."
119
+ },
120
+ {
121
+ slot: "activity",
122
+ recipe: "none",
123
+ part: "Root",
124
+ why: "Searching is the results' job: a list skeleton or a Spinner beside them, not a field that animates while you type."
125
+ }
126
+ ],
101
127
  a11y: [
102
128
  'Renders a native <input type="search"> \u2014 keyboard and form semantics come for free.',
103
129
  "Associate a <label htmlFor> (or wrap) \u2014 the magnifier glyph is decorative, not a label.",
@@ -16,7 +16,8 @@ var segmentedBarMeta = {
16
16
  },
17
17
  {
18
18
  guidance: "dont",
19
- description: "Use it for a single value against a limit; that is Meter, which carries the meter role and its value."
19
+ description: "Use it for a single value against a limit; that is Meter, which carries the meter role and its value.",
20
+ instead: ["Meter"]
20
21
  },
21
22
  {
22
23
  guidance: "dont",
@@ -29,11 +29,13 @@ var selectMeta = {
29
29
  },
30
30
  {
31
31
  guidance: "dont",
32
- description: "Use Select for very large or fuzzy lists. Use Combobox when typing is the primary way to find an option."
32
+ description: "Use Select for very large or fuzzy lists. Use Combobox when typing is the primary way to find an option.",
33
+ instead: ["Combobox"]
33
34
  },
34
35
  {
35
36
  guidance: "dont",
36
- description: "Use Select for binary on/off settings. Use Switch or Checkbox instead."
37
+ description: "Use Select for binary on/off settings. Use Switch or Checkbox instead.",
38
+ instead: ["Switch", "Checkbox"]
37
39
  },
38
40
  {
39
41
  guidance: "do",
@@ -30,11 +30,13 @@ var sheetMeta = {
30
30
  },
31
31
  {
32
32
  guidance: "dont",
33
- description: "Do not use Sheet for simple confirmations or deeply nested workflows."
33
+ description: "Do not use Sheet for simple confirmations or deeply nested workflows.",
34
+ instead: ["Dialog"]
34
35
  },
35
36
  {
36
37
  guidance: "dont",
37
- description: "Do not use it on desktop where a Dialog fits. A sheet's affordance is edge-anchored travel for thumb reach; on a wide viewport it becomes a dialog that arrives from the wrong place."
38
+ description: "Do not use it on desktop where a Dialog fits. A sheet's affordance is edge-anchored travel for thumb reach; on a wide viewport it becomes a dialog that arrives from the wrong place.",
39
+ instead: ["Dialog"]
38
40
  }
39
41
  ],
40
42
  anatomy: [
@@ -70,10 +70,7 @@ function ShowMore({
70
70
  Icon,
71
71
  {
72
72
  name: "chevron-down",
73
- className: cn(
74
- "transition-transform duration-(--motion-duration-fast) ease-standard",
75
- expanded && "rotate-180"
76
- )
73
+ className: cn("motion-interactive", expanded && "rotate-180")
77
74
  }
78
75
  ),
79
76
  children: expanded ? "Show less" : "Show more"
@@ -58,6 +58,44 @@ var showMoreMeta = {
58
58
  }
59
59
  ],
60
60
  tokensUsed: ["background", "foreground", "ring"],
61
+ motion: [
62
+ {
63
+ slot: "arrival",
64
+ recipe: "none",
65
+ part: "Root",
66
+ why: "The preview arrives with the content it clips. Its fade and toggle appear only once a measurement proves the content overflows."
67
+ },
68
+ {
69
+ slot: "response",
70
+ recipe: "motion-interactive",
71
+ part: "Toggle",
72
+ why: "The toggle is a link Button, so its hover and focus ease on Button's recipe."
73
+ },
74
+ {
75
+ slot: "transition",
76
+ recipe: "custom",
77
+ part: "Content",
78
+ why: "The region eases its max-height between the collapsed and the measured full height on the slow motion token. motion-disclosure keys on Base UI's starting and ending styles, which this self-contained toggle does not have. The text unrolling in place keeps the reader's line; a snap drops them at a different scroll position."
79
+ },
80
+ {
81
+ slot: "transition",
82
+ recipe: "motion-interactive",
83
+ part: "Fade",
84
+ why: "The bottom fade dissolves as the region opens and returns as it closes, so the cut edge and the reveal read as one movement."
85
+ },
86
+ {
87
+ slot: "transition",
88
+ recipe: "motion-interactive",
89
+ part: "Chevron",
90
+ why: "The chevron turns 180\xB0 in step with the region (motion-interactive lists `rotate`), so the toggle visibly points at what it will do next."
91
+ },
92
+ {
93
+ slot: "activity",
94
+ recipe: "none",
95
+ part: "Root",
96
+ why: "Expanding is local and instant; nothing loads."
97
+ }
98
+ ],
61
99
  a11y: [
62
100
  "The toggle is a real <button> with aria-expanded reflecting the open state.",
63
101
  "The fade overlay is pointer-events-none and decorative; it never traps clicks.",
@@ -4,19 +4,39 @@
4
4
  import { useRender } from "@base-ui/react/use-render";
5
5
  import { createContext, useContext } from "react";
6
6
  import { cn } from "../../lib/cn.js";
7
+ import { HighlightGlide } from "../../lib/highlight-glide.js";
8
+ import { motionAttribute } from "../../motion/MotionProvider.js";
7
9
  import { sideNavItemIconVariants, sideNavItemVariants } from "./side-nav.variants.js";
8
10
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
11
  var SideNavContext = createContext({
10
12
  size: "md",
11
13
  activeVariant: "content"
12
14
  });
15
+ var GLIDE_HANDOFF = "[&[data-glide]_[data-active]]:bg-transparent";
16
+ var GLIDE_FILL = {
17
+ content: "rounded-sm bg-accent motion-glide",
18
+ sidebar: "rounded-sm bg-sidebar-accent motion-glide"
19
+ };
13
20
  function SideNav({
14
21
  className,
15
22
  size = "md",
16
23
  activeVariant = "content",
24
+ motion,
25
+ children,
17
26
  ...props
18
27
  }) {
19
- return /* @__PURE__ */ jsx(SideNavContext.Provider, { value: { size, activeVariant }, children: /* @__PURE__ */ jsx("nav", { className: cn("flex flex-col gap-0.5", className), ...props }) });
28
+ return /* @__PURE__ */ jsx(SideNavContext.Provider, { value: { size, activeVariant }, children: /* @__PURE__ */ jsxs(
29
+ "nav",
30
+ {
31
+ "data-motion": motionAttribute(motion),
32
+ className: cn("flex flex-col gap-0.5", GLIDE_HANDOFF, className),
33
+ ...props,
34
+ children: [
35
+ /* @__PURE__ */ jsx(HighlightGlide, { follow: "data-active", timing: "selection", className: GLIDE_FILL[activeVariant] }),
36
+ children
37
+ ]
38
+ }
39
+ ) });
20
40
  }
21
41
  SideNav.displayName = "SideNav";
22
42
  function SideNavGroup({
@@ -33,7 +33,8 @@ var sideNavMeta = {
33
33
  },
34
34
  {
35
35
  guidance: "dont",
36
- description: "Use SideNav for bottom mobile navigation; use BottomNav in AppShell's bottomNav slot."
36
+ description: "Use SideNav for bottom mobile navigation; use BottomNav in AppShell's bottomNav slot.",
37
+ instead: ["BottomNav"]
37
38
  },
38
39
  {
39
40
  guidance: "dont",
@@ -79,6 +80,12 @@ var sideNavMeta = {
79
80
  type: '"content" | "sidebar"',
80
81
  default: '"content"',
81
82
  description: 'Active-state token vocabulary shared by all items. "content" uses bg-accent/text-accent-foreground for local rails inside the content area; "sidebar" uses bg-sidebar-accent/text-sidebar-accent-foreground for SideNavs rendered inside app chrome.'
83
+ },
84
+ {
85
+ name: "motion",
86
+ type: '"full" | "subtle" | "off"',
87
+ default: '"full"',
88
+ description: "How much motion this component uses: `full` inherits the app's policy, `subtle` keeps feedback and drops the current-page glide, `off` stills it. It can only turn motion down \u2014 it never overrides a reader's reduce-motion setting."
82
89
  }
83
90
  ],
84
91
  subcomponents: [
@@ -93,6 +100,32 @@ var sideNavMeta = {
93
100
  description: "A labelled section within a SideNav - groups settings into areas. label renders an optional sentence-case caption heading above the group's items."
94
101
  }
95
102
  ],
103
+ motion: [
104
+ {
105
+ slot: "arrival",
106
+ recipe: "none",
107
+ part: "Root",
108
+ why: "In-page navigation arrives with the page it navigates."
109
+ },
110
+ {
111
+ slot: "response",
112
+ recipe: "motion-interactive",
113
+ part: "SideNavItem",
114
+ why: "Rows ease their hover fill and focus ring, so moving down a long nav reads as one pointer travelling rather than rows flashing in turn."
115
+ },
116
+ {
117
+ slot: "transition",
118
+ recipe: "motion-glide",
119
+ part: "SideNav",
120
+ why: "The current-item fill is one element that travels to the new row when the section changes inside a persistent layout, so the marker reads as moving rather than one row dimming while another lights. It lands in place on first paint, each row keeps its own fill as the server-render fallback, and the active row's heavier weight lands at once \u2014 font weight does not ease. Expressive: `subtle` moves it at once."
121
+ },
122
+ {
123
+ slot: "activity",
124
+ recipe: "none",
125
+ part: "Root",
126
+ why: "Navigation chrome has no working state; a loading section shows it in the content."
127
+ }
128
+ ],
96
129
  a11y: [
97
130
  "SideNav renders a <nav> landmark; pair multiple navs with aria-label to distinguish them.",
98
131
  'The active item carries aria-current="page".',
@@ -2,7 +2,9 @@
2
2
  import { cva } from "class-variance-authority";
3
3
  var RAIL_ROW_INLINE = "gap-3 px-3";
4
4
  var sideNavItemVariants = cva(
5
- "flex w-full items-center text-start motion-interactive focus-visible:outline-none focus-visible:ring-2 data-[active]:font-semibold",
5
+ // `relative`: a row paints above SideNav's gliding current-page fill, which is
6
+ // absolutely positioned and earlier in the DOM.
7
+ "relative flex w-full items-center text-start motion-interactive focus-visible:outline-none focus-visible:ring-2 data-[active]:font-semibold",
6
8
  {
7
9
  variants: {
8
10
  size: {
@@ -4,9 +4,12 @@
4
4
  import { useRender } from "@base-ui/react/use-render";
5
5
  import {
6
6
  createContext,
7
- useContext
7
+ useContext,
8
+ useState
8
9
  } from "react";
9
10
  import { cn } from "../../lib/cn.js";
11
+ import { HighlightGlide } from "../../lib/highlight-glide.js";
12
+ import { motionAttribute } from "../../motion/MotionProvider.js";
10
13
  import { RAIL_ROW_INLINE, sideNavItemIconVariants, sideNavItemVariants } from "../side-nav/side-nav.variants.js";
11
14
  import { SimpleTooltip } from "../tooltip/index.js";
12
15
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -14,6 +17,7 @@ var SidebarContext = createContext({
14
17
  collapsed: false,
15
18
  icons: true
16
19
  });
20
+ var LABEL_FADE = "starting:opacity-0 transition-opacity duration-(--motion-duration-base) ease-decelerate";
17
21
  function useSidebar() {
18
22
  return useContext(SidebarContext);
19
23
  }
@@ -43,13 +47,17 @@ function Sidebar({
43
47
  icons = true,
44
48
  variant = "app",
45
49
  surface = "rail",
50
+ motion,
46
51
  className,
47
52
  children,
48
53
  ...props
49
54
  }) {
50
- return /* @__PURE__ */ jsx(SidebarContext.Provider, { value: { collapsed, icons }, children: /* @__PURE__ */ jsx(
55
+ const [hasCollapsed, setHasCollapsed] = useState(collapsed);
56
+ if (collapsed && !hasCollapsed) setHasCollapsed(true);
57
+ return /* @__PURE__ */ jsx(SidebarContext.Provider, { value: { collapsed, icons, reopened: hasCollapsed && !collapsed }, children: /* @__PURE__ */ jsx(
51
58
  "aside",
52
59
  {
60
+ "data-motion": motionAttribute(motion),
53
61
  "data-collapsed": collapsed || void 0,
54
62
  "data-variant": variant,
55
63
  "data-surface": surface,
@@ -66,6 +74,15 @@ function Sidebar({
66
74
  surface === "rail" ? cn("border-r border-sidebar-border", SIDEBAR_INK) : PAGE_INK,
67
75
  SIDEBAR_VARIANT[variant],
68
76
  "w-[var(--sidebar-width,16rem)] data-[collapsed]:w-[var(--sidebar-width-collapsed,4rem)]",
77
+ // Collapsing eases the width (`motion-size`, expressive) rather than
78
+ // jumping 192px in one frame. It reflows the page beside the rail for
79
+ // the length of the ease, which is the point: the content visibly
80
+ // takes the room the rail gave up. Nothing inside jumps, because a
81
+ // collapsed row's centred icon and an expanded row's inset icon sit
82
+ // on the same column (measured: 21px from the rail edge in both).
83
+ // Labels leave at once and fade back in as the rail reopens; the
84
+ // clip keeps them from painting past the rail while it is narrow.
85
+ "overflow-x-clip motion-size",
69
86
  className
70
87
  ),
71
88
  ...props,
@@ -94,7 +111,7 @@ function SidebarHeader({
94
111
  }
95
112
  SidebarHeader.displayName = "SidebarHeader";
96
113
  function SidebarBrand({ mark, label, render, className, "aria-label": ariaLabel, ...props }) {
97
- const { collapsed } = useSidebar();
114
+ const { collapsed, reopened } = useSidebar();
98
115
  return useRender({
99
116
  render: render ?? /* @__PURE__ */ jsx("div", {}),
100
117
  props: {
@@ -111,7 +128,7 @@ function SidebarBrand({ mark, label, render, className, "aria-label": ariaLabel,
111
128
  ),
112
129
  children: /* @__PURE__ */ jsxs(Fragment, { children: [
113
130
  /* @__PURE__ */ jsx("span", { className: sideNavItemIconVariants({ size: "rail" }), children: mark }),
114
- collapsed ? null : /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate text-title-sm", children: label })
131
+ collapsed ? null : /* @__PURE__ */ jsx("span", { className: cn("min-w-0 truncate text-title-sm", reopened && LABEL_FADE), children: label })
115
132
  ] })
116
133
  },
117
134
  defaultTagName: "div"
@@ -120,9 +137,10 @@ function SidebarBrand({ mark, label, render, className, "aria-label": ariaLabel,
120
137
  SidebarBrand.displayName = "SidebarBrand";
121
138
  function SidebarNav({
122
139
  className,
140
+ children,
123
141
  ...props
124
142
  }) {
125
- return /* @__PURE__ */ jsx(
143
+ return /* @__PURE__ */ jsxs(
126
144
  "nav",
127
145
  {
128
146
  className: cn(
@@ -130,9 +148,14 @@ function SidebarNav({
130
148
  // narrow rail, and contained overscroll so a wheel gesture that runs
131
149
  // out of nav stops there instead of scrolling the page behind it.
132
150
  "flex flex-1 flex-col gap-1 overflow-y-auto overscroll-contain px-3 py-4 scrollbar-quiet",
151
+ "[&[data-glide]_[data-active]]:bg-transparent",
133
152
  className
134
153
  ),
135
- ...props
154
+ ...props,
155
+ children: [
156
+ /* @__PURE__ */ jsx(HighlightGlide, { follow: "data-active", timing: "selection", className: "rounded-sm bg-sidebar-accent motion-glide" }),
157
+ children
158
+ ]
136
159
  }
137
160
  );
138
161
  }
@@ -163,7 +186,7 @@ function SidebarItem({
163
186
  "aria-label": ariaLabel,
164
187
  ...props
165
188
  }) {
166
- const { collapsed, icons } = useSidebar();
189
+ const { collapsed, icons, reopened } = useSidebar();
167
190
  const showIcon = icon != null && (icons || collapsed);
168
191
  const element = useRender({
169
192
  render: render ?? /* @__PURE__ */ jsx("button", { type: "button" }),
@@ -183,7 +206,7 @@ function SidebarItem({
183
206
  children: /* @__PURE__ */ jsxs(Fragment, { children: [
184
207
  showIcon ? /* @__PURE__ */ jsx("span", { className: sideNavItemIconVariants({ size: "rail" }), children: icon }) : null,
185
208
  collapsed ? null : /* @__PURE__ */ jsxs(Fragment, { children: [
186
- /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: label }),
209
+ /* @__PURE__ */ jsx("span", { className: cn("min-w-0 flex-1 truncate", reopened && LABEL_FADE), children: label }),
187
210
  badge ? /* @__PURE__ */ jsx("span", { className: "ms-auto shrink-0", children: badge }) : null
188
211
  ] })
189
212
  ] })
@@ -77,6 +77,12 @@ var sidebarMeta = {
77
77
  type: '"rail" | "page"',
78
78
  default: '"rail"',
79
79
  description: `Which surface the rail is painted on. "rail" is its own --sidebar-* surface with a border \u2014 right for a full-height rail that frames the app, which a theme may tint or paint dark. "page" uses the page's surface and ink with no border \u2014 use it when a full-width header sits above the rail, where a tinted column starting under a page-coloured bar reads as pasted on. Use it instead of overriding bg-/text- classes, which the rail's re-pointed tokens defeat.`
80
+ },
81
+ {
82
+ name: "motion",
83
+ type: '"full" | "subtle" | "off"',
84
+ default: '"full"',
85
+ description: "How much motion this component uses: `full` inherits the app's policy, `subtle` keeps feedback and drops the collapse ease and the current-page glide, `off` stills it. It can only turn motion down \u2014 it never overrides a reader's reduce-motion setting."
80
86
  }
81
87
  ],
82
88
  subcomponents: [
@@ -96,6 +102,38 @@ var sidebarMeta = {
96
102
  description: 'A single nav row. label is the row text (inline when expanded, in a hover tooltip when collapsed); icon is the leading icon node (the consumer swaps fill weight for the active state); active marks the current route (accent fill + aria-current="page"); badge is a trailing node (e.g. a count) pushed to the row end and hidden when collapsed; render composes another element, e.g. render={<Link to="/x" />}.'
97
103
  }
98
104
  ],
105
+ motion: [
106
+ {
107
+ slot: "arrival",
108
+ recipe: "none",
109
+ part: "Root",
110
+ why: "The rail is the app's frame, present from first paint. It is not summoned, so it has no entrance."
111
+ },
112
+ {
113
+ slot: "response",
114
+ recipe: "motion-interactive",
115
+ part: "SidebarItem",
116
+ why: "Rows ease their hover fill (SideNav's row recipe on the rail rung), so moving down the nav reads as the pointer travelling rather than rows flashing."
117
+ },
118
+ {
119
+ slot: "transition",
120
+ recipe: "motion-glide",
121
+ part: "SidebarNav",
122
+ why: "The current-page fill is one element that travels to the new item when the page changes inside a persistent layout, so the marker reads as moving rather than one row dimming while another lights. It lands in place on first paint, and each item keeps its own fill as the server-render fallback. Expressive: `subtle` moves it at once."
123
+ },
124
+ {
125
+ slot: "transition",
126
+ recipe: "motion-size",
127
+ part: "Root",
128
+ why: "Collapsing to the icon rail eases the width instead of jumping 192px in one frame, so the content beside it visibly takes the room the rail gave up. Nothing inside jumps \u2014 a collapsed row's centred icon and an expanded row's inset icon sit on the same column \u2014 and labels leave at once and fade back in on reopen, clipped to the rail meanwhile. It reflows the page for the length of the ease, which is acceptable for a deliberate user toggle and never happens on load. Expressive: `subtle` snaps."
129
+ },
130
+ {
131
+ slot: "activity",
132
+ recipe: "none",
133
+ part: "Root",
134
+ why: "Navigation chrome has no working state; a loading route shows it in the page."
135
+ }
136
+ ],
99
137
  a11y: [
100
138
  "SidebarNav renders a <nav> landmark.",
101
139
  'Active item carries aria-current="page".',
@@ -22,7 +22,8 @@ var skeletonMeta = {
22
22
  },
23
23
  {
24
24
  guidance: "dont",
25
- description: "Do not leave Skeleton visible after an error or empty state; switch to EmptyState (tone='destructive' for an error)."
25
+ description: "Do not leave Skeleton visible after an error or empty state; switch to EmptyState (tone='destructive' for an error).",
26
+ instead: ["EmptyState"]
26
27
  },
27
28
  {
28
29
  guidance: "dont",
@@ -173,6 +173,32 @@ var sliderMeta = {
173
173
  }
174
174
  ],
175
175
  tokensUsed: ["secondary", "primary", "background", "ring"],
176
+ motion: [
177
+ {
178
+ slot: "arrival",
179
+ recipe: "none",
180
+ part: "Root",
181
+ why: "It arrives with the form or panel it sits in."
182
+ },
183
+ {
184
+ slot: "response",
185
+ recipe: "motion-interactive",
186
+ part: "Thumb",
187
+ why: "The thumb's dot grows from 12px to full size on hover and keyboard focus, and a little past it while dragging, on a spring. Only the paint resizes, never the hit target, so the growth says the thumb is live without moving where you have to aim."
188
+ },
189
+ {
190
+ slot: "transition",
191
+ recipe: "none",
192
+ part: "Thumb",
193
+ why: "The thumb and the filled range follow the pointer one-to-one, and keyboard steps land at once. An eased position would trail the drag and show a value the user has already moved past."
194
+ },
195
+ {
196
+ slot: "activity",
197
+ recipe: "none",
198
+ part: "Root",
199
+ why: "A slider commits as it moves; there is nothing to wait for."
200
+ }
201
+ ],
176
202
  a11y: [
177
203
  "Base UI provides role=slider, aria-valuemin/max/now, and arrow-key / Page Up-Down / Home-End control.",
178
204
  "Each thumb is a focusable input[type=range] nested in the thumb element; keyboard focus raises a 2px ring on the thumb via has-[:focus-visible] (a bare focus-visible: on the thumb never fires, since the div itself is not focusable).",
@@ -5,15 +5,17 @@ import { areaPath, linePath } from "../../lib/chart/geometry.js";
5
5
  import { drawTransition, riseTransition, stagger } from "../../lib/chart/motion.js";
6
6
  import { toneColor } from "../../lib/chart/types.js";
7
7
  import { cn } from "../../lib/cn.js";
8
- import { useMotion } from "../../motion/MotionProvider.js";
8
+ import { motionAttribute, useMotionLevel } from "../../motion/MotionProvider.js";
9
+ import { chartMotion } from "../../lib/chart/chart-frame.js";
9
10
  import { useArrival } from "../../lib/chart/use-arrival.js";
10
11
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
11
12
  var W = 100;
12
13
  var H = 32;
13
- function Sparkline({ data, label, variant = "line", tone = "primary", animate = true, locale, className, ...props }) {
14
- const { enabled } = useMotion();
14
+ function Sparkline({ data, label, variant = "line", tone = "primary", motion, animate, locale, className, ...props }) {
15
+ const level = chartMotion(motion, animate);
16
+ const { expressive } = useMotionLevel(level);
15
17
  const [ref, inView] = useArrival();
16
- const shown = !animate || !enabled || inView;
18
+ const shown = !expressive || inView;
17
19
  const color = toneColor(tone);
18
20
  const min = data.length ? Math.min(...data) : 0;
19
21
  const max = data.length ? Math.max(...data) : 1;
@@ -72,6 +74,7 @@ function Sparkline({ data, label, variant = "line", tone = "primary", animate =
72
74
  viewBox: `0 0 ${W} ${H}`,
73
75
  preserveAspectRatio: "none",
74
76
  "data-reveal": shown ? "in" : "out",
77
+ "data-motion": motionAttribute(level),
75
78
  className: cn("inline-block h-8 w-24 shrink-0 overflow-visible align-middle", className),
76
79
  ...props,
77
80
  children: marks
@@ -70,11 +70,18 @@ var sparklineMeta = {
70
70
  default: '"h-8 w-24"',
71
71
  description: "Sizes it; the drawing stretches to fit."
72
72
  },
73
+ {
74
+ name: "motion",
75
+ type: '"full" | "subtle" | "off"',
76
+ default: '"full"',
77
+ 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. Drawing on (or rising) is the flourish, so `subtle` and `off` show the finished line."
78
+ },
73
79
  {
74
80
  name: "animate",
75
81
  type: "boolean",
76
82
  default: "true",
77
- description: "Draw on (or rise) when it scrolls into view."
83
+ description: 'Draw on (or rise) when it scrolls into view. `animate={false}` is `motion="subtle"`.',
84
+ deprecated: 'motion="subtle" (what animate={false} now maps to) or motion="off"'
78
85
  },
79
86
  {
80
87
  name: "locale",
@@ -106,7 +113,7 @@ var sparklineMeta = {
106
113
  slot: "arrival",
107
114
  recipe: "custom",
108
115
  part: "Line / bars",
109
- why: "Once on screen the line draws on (or the bars rise, in sequence). On the motion tokens; reduced motion shows the finished drawing."
116
+ why: 'Once on screen the line draws on (or the bars rise, in sequence). On the motion tokens; reduced motion shows the finished drawing. The arrival is expressive: it plays at the full motion level only, so `motion="subtle"` (or a subtle app policy) shows the finished line.'
110
117
  },
111
118
  {
112
119
  slot: "response",
@@ -22,7 +22,8 @@ var spinnerMeta = {
22
22
  },
23
23
  {
24
24
  guidance: "dont",
25
- description: "Do not use Spinner as the only content for long waits or full page loading."
25
+ description: "Do not use Spinner as the only content for long waits or full page loading.",
26
+ instead: ["Skeleton", "PageLoader"]
26
27
  },
27
28
  {
28
29
  guidance: "dont",
@@ -30,11 +30,13 @@ var stepperMeta = {
30
30
  },
31
31
  {
32
32
  guidance: "dont",
33
- description: "Do not use Stepper for a task with unknown length or for a percentage. That is Progress; a stepper promises a fixed set of named stops."
33
+ description: "Do not use Stepper for a task with unknown length or for a percentage. That is Progress; a stepper promises a fixed set of named stops.",
34
+ instead: ["Progress"]
34
35
  },
35
36
  {
36
37
  guidance: "dont",
37
- description: "Do not use it as navigation between independent sections. Sections the user can visit in any order are Tabs; a stepper implies each step depends on the one before."
38
+ description: "Do not use it as navigation between independent sections. Sections the user can visit in any order are Tabs; a stepper implies each step depends on the one before.",
39
+ instead: ["Tabs"]
38
40
  },
39
41
  {
40
42
  guidance: "dont",
@@ -26,7 +26,8 @@ var switchMeta = {
26
26
  },
27
27
  {
28
28
  guidance: "dont",
29
- description: "Do not use Switch for one-time agreement, multi-select choices, or submit-only booleans."
29
+ description: "Do not use Switch for one-time agreement, multi-select choices, or submit-only booleans.",
30
+ instead: ["Checkbox", "Radio"]
30
31
  },
31
32
  {
32
33
  guidance: "dont",
@@ -34,7 +34,8 @@ var tableMeta = {
34
34
  },
35
35
  {
36
36
  guidance: "dont",
37
- description: "Use Table for heterogeneous items with no stable columns. Use Card, Entity, or a list composition instead."
37
+ description: "Use Table for heterogeneous items with no stable columns. Use Card, Entity, or a list composition instead.",
38
+ instead: ["Card", "Entity"]
38
39
  },
39
40
  {
40
41
  guidance: "dont",
@@ -114,6 +115,32 @@ var tableMeta = {
114
115
  }
115
116
  ],
116
117
  tokensUsed: ["border", "muted", "muted-foreground"],
118
+ motion: [
119
+ {
120
+ slot: "arrival",
121
+ recipe: "none",
122
+ part: "Root",
123
+ why: "A table renders with the page it sits in. Staggering rows in delays reading data someone opened to scan."
124
+ },
125
+ {
126
+ slot: "response",
127
+ recipe: "motion-interactive",
128
+ part: "TableRow",
129
+ why: "Body rows ease to a muted wash under the pointer. Across a wide row, the wash is what keeps the eye on the right line, and eased it follows the cursor down the table rather than strobing."
130
+ },
131
+ {
132
+ slot: "transition",
133
+ recipe: "motion-interactive",
134
+ part: "TableCell",
135
+ why: "A `data-selected` row's fill eases in on every cell together, so the selected row reads as one band being set rather than a stamp."
136
+ },
137
+ {
138
+ slot: "activity",
139
+ recipe: "none",
140
+ part: "Root",
141
+ why: "Table is markup. Loading rows are Skeletons, which carry the shimmer."
142
+ }
143
+ ],
117
144
  a11y: [
118
145
  "Renders semantic native table elements (thead/tbody/tfoot/th/td/caption) preserving built-in table semantics.",
119
146
  "Use TableHead (th) for column headers and TableCaption for an accessible description.",