@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
@@ -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".',
@@ -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",
@@ -115,6 +115,32 @@ var tableMeta = {
115
115
  }
116
116
  ],
117
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
+ ],
118
144
  a11y: [
119
145
  "Renders semantic native table elements (thead/tbody/tfoot/th/td/caption) preserving built-in table semantics.",
120
146
  "Use TableHead (th) for column headers and TableCaption for an accessible description.",
@@ -3,19 +3,38 @@ import { Tabs as Base } from "@base-ui/react/tabs";
3
3
  import { createContext, useContext } from "react";
4
4
  import { cn } from "../../lib/cn.js";
5
5
  import { trimLabel } from "../../lib/trim-label.js";
6
- import { jsx } from "react/jsx-runtime";
7
- var Tabs = Base.Root;
6
+ import { motionAttribute } from "../../motion/MotionProvider.js";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ function Tabs({ motion, ...props }) {
9
+ return /* @__PURE__ */ jsx(Base.Root, { "data-motion": motionAttribute(motion), ...props });
10
+ }
11
+ Tabs.displayName = "Tabs";
8
12
  var TabsVariantContext = createContext("underline");
9
- function TabsList({ className, variant = "underline", ...props }) {
10
- return /* @__PURE__ */ jsx(TabsVariantContext.Provider, { value: variant, children: /* @__PURE__ */ jsx(
13
+ var UNDERLINE_HANDOFF = "[&:has(>[data-tabs-indicator]:not([hidden]))>[role=tab]]:border-transparent";
14
+ var PILL_HANDOFF = "[&:has(>[data-tabs-indicator]:not([hidden]))>[role=tab]]:bg-transparent";
15
+ var INDICATOR = "pointer-events-none absolute left-0 w-(--active-tab-width) translate-x-(--active-tab-left) motion-glide";
16
+ var INDICATOR_UNDERLINE = "-bottom-px h-0.5 bg-foreground";
17
+ var INDICATOR_PILL = "top-0 h-(--active-tab-height) translate-y-(--active-tab-top) rounded-[var(--button-radius,9999px)] bg-muted";
18
+ function TabsList({ className, variant = "underline", children, ...props }) {
19
+ return /* @__PURE__ */ jsx(TabsVariantContext.Provider, { value: variant, children: /* @__PURE__ */ jsxs(
11
20
  Base.List,
12
21
  {
13
22
  "data-variant": variant,
14
23
  className: cn(
15
- variant === "underline" ? "inline-flex items-center gap-1 border-b border-border" : "inline-flex items-center gap-1",
24
+ variant === "underline" ? cn("relative inline-flex items-center gap-1 border-b border-border", UNDERLINE_HANDOFF) : cn("relative inline-flex items-center gap-1", PILL_HANDOFF),
16
25
  className
17
26
  ),
18
- ...props
27
+ ...props,
28
+ children: [
29
+ /* @__PURE__ */ jsx(
30
+ Base.Indicator,
31
+ {
32
+ "data-tabs-indicator": "",
33
+ className: cn(INDICATOR, variant === "underline" ? INDICATOR_UNDERLINE : INDICATOR_PILL)
34
+ }
35
+ ),
36
+ children
37
+ ]
19
38
  }
20
39
  ) });
21
40
  }
@@ -25,7 +44,7 @@ function TabsTrigger({ className, children, ...props }) {
25
44
  Base.Tab,
26
45
  {
27
46
  className: cn(
28
- "cursor-pointer text-label-lg text-muted-foreground motion-interactive hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring data-[active]:text-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
47
+ "relative cursor-pointer text-label-lg text-muted-foreground motion-interactive hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring data-[active]:text-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
29
48
  variant === "underline" ? (
30
49
  // On the control ladder like the pill variant (it was py-2 plus
31
50
  // the label's line, ~36px — no rung).
@@ -43,7 +62,7 @@ function TabsPanel({ className, ...props }) {
43
62
  Base.Panel,
44
63
  {
45
64
  className: cn(
46
- "pt-4 text-foreground focus-visible:outline-none",
65
+ "pt-4 text-foreground focus-visible:outline-none not-data-[activation-direction=none]:animate-fade-in data-[ending-style]:hidden",
47
66
  className
48
67
  ),
49
68
  ...props
@@ -1,7 +1,7 @@
1
1
  // src/components/tabs/tabs.meta.ts
2
2
  var tabsMeta = {
3
3
  name: "Tabs",
4
- summary: "Tabs over Base UI Tabs (roving focus, arrow-key nav, ARIA tablist wiring) in two styles: underline (page sections) or pill (compact switchers). Tabs (Root) + TabsList + TabsTrigger + TabsPanel.",
4
+ summary: "Tabs over Base UI Tabs (roving focus, arrow-key nav, ARIA tablist wiring) in two styles: underline (page sections) or pill (compact switchers), with one indicator that glides to the active tab. Tabs (Root) + TabsList + TabsTrigger + TabsPanel.",
5
5
  status: "stable",
6
6
  keywords: [
7
7
  "tabs",
@@ -71,13 +71,19 @@ var tabsMeta = {
71
71
  type: "Base UI Tabs.Root props (value, defaultValue, onValueChange, orientation)",
72
72
  default: "defaultValue: 0",
73
73
  description: "Controls the active tab. Uncontrolled via defaultValue; controlled via value + onValueChange."
74
+ },
75
+ {
76
+ name: "motion",
77
+ type: '"full" | "subtle" | "off"',
78
+ default: '"full"',
79
+ description: "On Tabs: 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."
74
80
  }
75
81
  ],
76
82
  subcomponents: [
77
83
  {
78
84
  name: "TabsList",
79
85
  type: 'Base UI Tabs.List props (activateOnFocus, loopFocus) + variant?: "underline" | "pill" + className',
80
- description: `The tablist row. variant="underline" (default) draws a divider rule beneath the triggers; variant="pill" drops the rule and fills the active trigger, which takes the theme's button shape (--button-radius). Triggers inherit the variant.`
86
+ description: `The tablist row. variant="underline" (default) draws a divider rule beneath the triggers; variant="pill" drops the rule and fills the active trigger, which takes the theme's button shape (--button-radius). Triggers inherit the variant. It renders the gliding indicator (Base UI Tabs.Indicator) itself, so do not add one.`
81
87
  },
82
88
  {
83
89
  name: "TabsTrigger",
@@ -96,11 +102,44 @@ var tabsMeta = {
96
102
  }
97
103
  ],
98
104
  tokensUsed: ["border", "muted", "muted-foreground", "foreground", "ring"],
105
+ motion: [
106
+ {
107
+ slot: "arrival",
108
+ recipe: "none",
109
+ part: "Root",
110
+ why: "Tabs arrive with the section they organise. An entrance of their own would announce navigation chrome louder than the content it navigates."
111
+ },
112
+ {
113
+ slot: "response",
114
+ recipe: "motion-interactive",
115
+ part: "TabsTrigger",
116
+ why: "The label steps from muted to foreground under the pointer; the ease is what makes that read as an answer rather than a repaint."
117
+ },
118
+ {
119
+ slot: "transition",
120
+ recipe: "motion-glide",
121
+ part: "Indicator",
122
+ why: `One underline (or pill) travels to the new tab and stretches to its width. A mark that teleports makes the reader re-find the selection on every click; one that moves is tracked without looking. Expressive: motion="subtle" lands it in place. Each tab's own mark is the fallback until the indicator has measured, so the server render is already right.`
123
+ },
124
+ {
125
+ slot: "transition",
126
+ recipe: "animate-fade-in",
127
+ part: "TabsPanel",
128
+ why: 'The incoming panel fades in, opacity only, so a switch between two similar panels still registers. Skipped on first render (activation direction "none") so a page of tabs does not fade in on load, and the outgoing panel leaves layout at once so a quick second click never stacks both.'
129
+ },
130
+ {
131
+ slot: "activity",
132
+ recipe: "none",
133
+ part: "Root",
134
+ why: "Switching tabs is synchronous. A panel that loads data shows its own Skeleton or Spinner inside the panel, where the wait is."
135
+ }
136
+ ],
99
137
  a11y: [
100
138
  "Base UI provides the tablist/tab/tabpanel roles, aria-selected, and roving tabindex.",
101
139
  "Arrow keys move between tabs; Home/End jump to first/last; panels are linked via aria-controls.",
102
140
  "Active tab styling reads from the data-active attribute; disabled tabs from data-disabled. Disabled tabs are skipped by arrow-key navigation.",
103
- "Keyboard focus shows a ring on the focused trigger in both variants; the panel remains focusable."
141
+ "Keyboard focus shows a ring on the focused trigger in both variants; the panel remains focusable.",
142
+ 'The gliding indicator is role="presentation" decoration; aria-selected on the tab carries the state.'
104
143
  ],
105
144
  examples: [
106
145
  {
@@ -114,6 +153,10 @@ var tabsMeta = {
114
153
  {
115
154
  label: "With a disabled tab",
116
155
  code: '<Tabs defaultValue="overview"><TabsList><TabsTrigger value="overview">Overview</TabsTrigger><TabsTrigger value="billing" disabled>Billing</TabsTrigger></TabsList><TabsPanel value="overview">Overview content</TabsPanel></Tabs>'
156
+ },
157
+ {
158
+ label: "Subtle motion",
159
+ code: '<Tabs defaultValue="table" motion="subtle"><TabsList><TabsTrigger value="table">Table</TabsTrigger><TabsTrigger value="chart">Chart</TabsTrigger></TabsList><TabsPanel value="table">Rows</TabsPanel><TabsPanel value="chart">Trend</TabsPanel></Tabs>'
117
160
  }
118
161
  ]
119
162
  };
@@ -117,6 +117,38 @@ var themeSelectorMeta = {
117
117
  "muted-foreground",
118
118
  "ring"
119
119
  ],
120
+ motion: [
121
+ {
122
+ slot: "arrival",
123
+ recipe: "none",
124
+ part: "Root",
125
+ why: "It sits in a settings panel or a popover, which owns the entrance."
126
+ },
127
+ {
128
+ slot: "response",
129
+ recipe: "motion-interactive",
130
+ part: "Option (list)",
131
+ why: "List rows ease their hover fill like Menu and Select rows, so a theme list beside a menu reads as the same kind of row."
132
+ },
133
+ {
134
+ slot: "response",
135
+ recipe: "motion-interactive",
136
+ part: "Name (grid)",
137
+ why: "In the grid, the theme's name brightens on hover, eased. The preview tile's outline steps rather than easing \u2014 outline is not in the recipe's transition list \u2014 so the name carries the motion."
138
+ },
139
+ {
140
+ slot: "transition",
141
+ recipe: "motion-interactive",
142
+ part: "Check (list)",
143
+ why: "The check fades into the chosen row and out of the old one, so the choice reads as moving, at the same moment the whole interface repaints in the new theme."
144
+ },
145
+ {
146
+ slot: "activity",
147
+ recipe: "none",
148
+ part: "Root",
149
+ why: "Applying a theme is synchronous: the whole interface repaints at once, and that repaint is the feedback."
150
+ }
151
+ ],
120
152
  a11y: [
121
153
  'The container is role="radiogroup" with an aria-label; each card is a role="radio" button with aria-checked reflecting the selected theme.',
122
154
  "Standard radio keyboard: arrow keys move the check \u2014 calling onValueChange on each press \u2014 and Space checks the focused option. That instant switching is right for a free, reversible choice; where a switch has a cost, use a DisclosurePicker list (a listbox: arrows move, Enter commits) with ThemePreview tiles instead.",
@@ -3,6 +3,7 @@
3
3
  // src/components/theme-switcher/theme-switcher.tsx
4
4
  import { Radio as RadioBase } from "@base-ui/react/radio";
5
5
  import { Icon } from "../../icon/index.js";
6
+ import { motionAttribute } from "../../motion/MotionProvider.js";
6
7
  import { cn } from "../../lib/cn.js";
7
8
  import {
8
9
  SEGMENTED_INNER_RADIUS,
@@ -21,6 +22,7 @@ function ThemeSwitcher({
21
22
  value,
22
23
  onValueChange,
23
24
  label = "Theme",
25
+ motion,
24
26
  className,
25
27
  ref,
26
28
  ...props
@@ -31,6 +33,7 @@ function ThemeSwitcher({
31
33
  {
32
34
  ref,
33
35
  "aria-label": label,
36
+ "data-motion": motionAttribute(motion),
34
37
  value,
35
38
  onValueChange: (next) => onValueChange(next),
36
39
  className: cn(
@@ -54,7 +57,7 @@ function ThemeSwitcher({
54
57
  {
55
58
  "aria-hidden": true,
56
59
  className: cn(
57
- "pointer-events-none absolute start-0.5 top-0.5 size-7 bg-accent transition-[translate,opacity] duration-(--motion-duration-base) ease-emphasized",
60
+ "pointer-events-none absolute start-0.5 top-0.5 size-7 bg-accent motion-glide",
58
61
  SEGMENTED_INNER_RADIUS,
59
62
  "[translate:calc(var(--segment-index)*var(--segment-step))_0] rtl:[translate:calc(var(--segment-index)*var(--segment-step)*-1)_0]",
60
63
  activeIndex < 0 && "opacity-0"
@@ -58,6 +58,12 @@ var themeSwitcherMeta = {
58
58
  default: '"Theme"',
59
59
  description: "Accessible label for the radiogroup."
60
60
  },
61
+ {
62
+ name: "motion",
63
+ type: '"full" | "subtle" | "off"',
64
+ default: '"full"',
65
+ 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."
66
+ },
61
67
  {
62
68
  name: "className",
63
69
  type: "string",
@@ -73,6 +79,38 @@ var themeSwitcherMeta = {
73
79
  "accent-foreground",
74
80
  "ring"
75
81
  ],
82
+ motion: [
83
+ {
84
+ slot: "arrival",
85
+ recipe: "none",
86
+ part: "Root",
87
+ why: "It arrives with the header or settings row it lives in; a theme control that announces itself competes with the page it themes."
88
+ },
89
+ {
90
+ slot: "response",
91
+ recipe: "motion-interactive",
92
+ part: "Option",
93
+ why: "The icon steps from muted to foreground under the pointer, which is what tells an icon-only segment it is a control."
94
+ },
95
+ {
96
+ slot: "response",
97
+ recipe: "motion-press",
98
+ part: "Option",
99
+ why: "A settle under the press confirms the tap on a 28px target before the theme change repaints the page around it."
100
+ },
101
+ {
102
+ slot: "transition",
103
+ recipe: "motion-glide",
104
+ part: "Active pill",
105
+ why: 'One pill slides to the chosen mode, stepping by option + gap on the spacing scale, so light \u2192 dark reads as the same control moving, not three icons trading a fill. Expressive: motion="subtle" lands it in place.'
106
+ },
107
+ {
108
+ slot: "activity",
109
+ recipe: "none",
110
+ part: "Root",
111
+ why: "Changing the theme is synchronous; the repaint is the feedback."
112
+ }
113
+ ],
76
114
  a11y: [
77
115
  'The container is role="radiogroup" with an aria-label; each option is role="radio" with aria-checked reflecting the selected value.',
78
116
  "Each icon button carries an aria-label (Light / Dark / System) plus a title tooltip, since the visual is icon-only.",
@@ -91,6 +129,10 @@ var themeSwitcherMeta = {
91
129
  {
92
130
  label: "Light selected",
93
131
  code: '<ThemeSwitcher value="light" onValueChange={setTheme} />'
132
+ },
133
+ {
134
+ label: "Subtle motion",
135
+ code: '<ThemeSwitcher value={theme} onValueChange={setTheme} motion="subtle" />'
94
136
  }
95
137
  ]
96
138
  };
@@ -5,6 +5,7 @@ import { Icon } from "../../icon/index.js";
5
5
  import { cn } from "../../lib/cn.js";
6
6
  import { Button } from "../button/button.js";
7
7
  import { hitTargetMin } from "../../lib/hit-target.js";
8
+ import { STATUS_TONE_ICON } from "../../lib/status-tone.js";
8
9
  import { withPortalThemeScope } from "../../lib/portal-theme-scope.js";
9
10
  import { Spinner } from "../spinner/spinner.js";
10
11
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -51,7 +52,11 @@ var STACKED_ROOT = [
51
52
  // steps down to shadow-md and the cards behind to shadow-sm; fanned out,
52
53
  // each is a toast again and takes the full step back.
53
54
  "not-data-expanded:shadow-md not-data-expanded:has-[>[data-behind]]:shadow-sm",
54
- "transition-[transform,translate,opacity,height,box-shadow] duration-(--motion-duration-slow) ease-emphasized"
55
+ // The deck springs: cards stepping back and fanning out overshoot a touch
56
+ // and settle, which is what makes them read as cards rather than rows. A
57
+ // leaving toast takes the standard curve (nothing overshoots on its way
58
+ // out), and under "subtle" the spring is a plain decelerate.
59
+ "transition-[transform,translate,opacity,height,box-shadow] duration-(--motion-duration-slow) ease-spring data-ending-style:ease-standard"
55
60
  ].join(" ");
56
61
  var STACKED_EDGE = {
57
62
  top: "top-0 origin-top [--toast-dir:1] after:top-full",
@@ -204,11 +209,11 @@ function ToastProvider({
204
209
  )
205
210
  ] });
206
211
  }
207
- var TYPE_ACCENT = {
208
- success: "bg-success",
209
- error: "bg-destructive",
210
- warning: "bg-tone-warning",
211
- info: "bg-tone-info"
212
+ var TYPE_TONE = {
213
+ success: "success",
214
+ error: "destructive",
215
+ warning: "warning",
216
+ info: "info"
212
217
  };
213
218
  function ToastList({
214
219
  closeIcon,
@@ -218,7 +223,8 @@ function ToastList({
218
223
  }) {
219
224
  const { toasts } = useToast();
220
225
  return /* @__PURE__ */ jsx(ToastViewport, { position, stacked, className: viewportClassName, children: toasts.map((item) => {
221
- const accent = item.type ? TYPE_ACCENT[item.type] : void 0;
226
+ const tone = item.type ? TYPE_TONE[item.type] : void 0;
227
+ const status = tone ? STATUS_TONE_ICON[tone] : void 0;
222
228
  return /* @__PURE__ */ jsx(ToastRoot, { toast: item, children: /* @__PURE__ */ jsxs(ToastContent, { children: [
223
229
  item.type === "loading" ? /* @__PURE__ */ jsx(
224
230
  Spinner,
@@ -226,12 +232,18 @@ function ToastList({
226
232
  "aria-hidden": true,
227
233
  className: "mt-0.5 size-4 shrink-0 text-muted-foreground"
228
234
  }
229
- ) : accent ? /* @__PURE__ */ jsx(
230
- "span",
231
- {
232
- "aria-hidden": true,
233
- className: cn("w-1 shrink-0 self-stretch rounded-full", accent)
234
- }
235
+ ) : status ? (
236
+ // Blooms in once the card has landed, and again when a
237
+ // loading toast settles into success or failure: the
238
+ // glyph mounts in the spinner's place.
239
+ /* @__PURE__ */ jsx(
240
+ "span",
241
+ {
242
+ "aria-hidden": true,
243
+ className: "mt-0.5 flex shrink-0 motion-bloom [--motion-bloom-delay:120ms]",
244
+ children: /* @__PURE__ */ jsx(Icon, { name: status.name, className: cn("text-base", status.className) })
245
+ }
246
+ )
235
247
  ) : null,
236
248
  /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [
237
249
  item.title ? /* @__PURE__ */ jsx(ToastTitle, { children: item.title }) : null,