@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.
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +172 -0
- package/CONTRIBUTING.md +6 -4
- package/DESIGN.md +10 -3
- package/dist/blocks/approval-block.js +93 -0
- package/dist/blocks/approval-block.meta.js +23 -0
- package/dist/blocks/index.d.ts +27 -1
- package/dist/blocks/index.js +8 -0
- package/dist/blocks/message-reactions-block.js +87 -0
- package/dist/blocks/message-reactions-block.meta.js +23 -0
- package/dist/components/accordion/accordion.meta.js +8 -2
- package/dist/components/area-chart/area-chart.meta.js +10 -3
- package/dist/components/attachment/attachment.meta.js +26 -0
- package/dist/components/bar-chart/bar-chart.meta.js +11 -4
- package/dist/components/bottom-nav/bottom-nav.js +3 -1
- package/dist/components/bottom-nav/bottom-nav.meta.js +32 -0
- package/dist/components/breadcrumbs/breadcrumbs.meta.js +26 -0
- package/dist/components/bubble/bubble.js +105 -5
- package/dist/components/bubble/bubble.meta.js +42 -6
- package/dist/components/bubble/bubble.variants.js +1 -1
- package/dist/components/bubble/index.js +2 -0
- package/dist/components/button/button.js +21 -4
- package/dist/components/button/button.meta.js +7 -7
- package/dist/components/calendar/calendar.js +18 -1
- package/dist/components/calendar/calendar.meta.js +38 -0
- package/dist/components/carousel/carousel.js +6 -4
- package/dist/components/carousel/carousel.meta.js +42 -0
- package/dist/components/chat-composer/chat-composer.js +21 -21
- package/dist/components/chat-composer/chat-composer.meta.js +38 -0
- package/dist/components/chat-composer/use-eased-growth.js +70 -0
- package/dist/components/checkbox/checkbox-mark.js +8 -1
- package/dist/components/checkbox/checkbox.js +3 -1
- package/dist/components/checkbox/checkbox.meta.js +32 -0
- package/dist/components/checkbox-card/checkbox-card.js +3 -0
- package/dist/components/checkbox-card/checkbox-card.meta.js +32 -0
- package/dist/components/code-block/code-block.js +14 -3
- package/dist/components/code-block/code-block.meta.js +28 -1
- package/dist/components/collapsible/collapsible.meta.js +32 -0
- package/dist/components/color-field/color-field.meta.js +26 -0
- package/dist/components/combobox/combobox.meta.js +44 -0
- package/dist/components/command-menu/command-menu.js +8 -3
- package/dist/components/command-menu/command-menu.meta.js +38 -0
- package/dist/components/confirm-button/confirm-button.js +179 -0
- package/dist/components/confirm-button/confirm-button.meta.js +143 -0
- package/dist/components/confirm-button/index.js +7 -0
- package/dist/components/data-table/data-table.js +6 -2
- package/dist/components/data-table/data-table.meta.js +32 -0
- package/dist/components/entity/entity.meta.js +26 -0
- package/dist/components/feedback/feedback.js +120 -47
- package/dist/components/feedback/feedback.meta.js +55 -4
- package/dist/components/input-group/input-group.meta.js +27 -1
- package/dist/components/like-button/index.js +7 -0
- package/dist/components/like-button/like-button.js +89 -0
- package/dist/components/like-button/like-button.meta.js +132 -0
- package/dist/components/line-chart/line-chart.meta.js +10 -3
- package/dist/components/middle-truncate/middle-truncate.meta.js +1 -1
- package/dist/components/note/note.meta.js +32 -0
- package/dist/components/notification-badge/notification-badge.js +17 -5
- package/dist/components/notification-badge/notification-badge.meta.js +34 -2
- package/dist/components/number-ticker/index.js +7 -0
- package/dist/components/number-ticker/number-ticker.js +246 -0
- package/dist/components/number-ticker/number-ticker.meta.js +159 -0
- package/dist/components/otp-field/otp-field.js +23 -3
- package/dist/components/otp-field/otp-field.meta.js +23 -2
- package/dist/components/panel-group/panel-group.meta.js +26 -0
- package/dist/components/panel-section/panel-section.js +37 -10
- package/dist/components/panel-section/panel-section.meta.js +32 -0
- package/dist/components/pie-chart/pie-chart.js +20 -11
- package/dist/components/pie-chart/pie-chart.meta.js +17 -4
- package/dist/components/pill/pill.meta.js +32 -0
- package/dist/components/radar-chart/radar-chart.js +4 -3
- package/dist/components/radar-chart/radar-chart.meta.js +10 -3
- package/dist/components/radial-chart/radial-chart.js +22 -22
- package/dist/components/radial-chart/radial-chart.meta.js +17 -4
- package/dist/components/radio/radio.meta.js +26 -0
- package/dist/components/radio-card/radio-card.meta.js +38 -0
- package/dist/components/reaction-picker/index.js +13 -0
- package/dist/components/reaction-picker/reaction-picker.js +104 -0
- package/dist/components/reaction-picker/reaction-picker.meta.js +156 -0
- package/dist/components/relative-time/relative-time.meta.js +32 -0
- package/dist/components/scroller/scroller.meta.js +32 -0
- package/dist/components/scrub-field/scrub-field.meta.js +32 -0
- package/dist/components/search-input/search-input.js +9 -7
- package/dist/components/search-input/search-input.meta.js +26 -0
- package/dist/components/show-more/show-more.js +1 -4
- package/dist/components/show-more/show-more.meta.js +38 -0
- package/dist/components/side-nav/side-nav.js +21 -1
- package/dist/components/side-nav/side-nav.meta.js +32 -0
- package/dist/components/side-nav/side-nav.variants.js +3 -1
- package/dist/components/sidebar/sidebar.js +31 -8
- package/dist/components/sidebar/sidebar.meta.js +38 -0
- package/dist/components/slider/slider.meta.js +26 -0
- package/dist/components/sparkline/sparkline.js +7 -4
- package/dist/components/sparkline/sparkline.meta.js +9 -2
- package/dist/components/table/table.meta.js +26 -0
- package/dist/components/tabs/tabs.js +27 -8
- package/dist/components/tabs/tabs.meta.js +46 -3
- package/dist/components/theme-selector/theme-selector.meta.js +32 -0
- package/dist/components/theme-switcher/theme-switcher.js +4 -1
- package/dist/components/theme-switcher/theme-switcher.meta.js +42 -0
- package/dist/components/toast/toast.js +25 -13
- package/dist/components/toast/toast.meta.js +7 -1
- package/dist/components/toggle/toggle.js +28 -5
- package/dist/components/toggle/toggle.meta.js +44 -2
- package/dist/components/tool-call/index.js +8 -0
- package/dist/components/tool-call/tool-call.js +117 -0
- package/dist/components/tool-call/tool-call.meta.js +147 -0
- package/dist/components/tool-call/tool-call.variants.js +33 -0
- package/dist/components/tooltip/index.js +3 -1
- package/dist/components/tooltip/tooltip.js +84 -0
- package/dist/components/tooltip/tooltip.meta.js +16 -1
- package/dist/components/tree/tree.js +71 -6
- package/dist/components/tree/tree.meta.js +32 -0
- package/dist/components/video/video.meta.js +32 -0
- package/dist/file-hashes.json +130 -104
- package/dist/foundations/motion.js +69 -3
- package/dist/icon-sets/reicon.js +11 -1
- package/dist/index.d.ts +907 -104
- package/dist/index.js +6 -1
- package/dist/lib/chart/cartesian.js +9 -10
- package/dist/lib/chart/chart-center.js +30 -0
- package/dist/lib/chart/chart-frame.js +15 -8
- package/dist/lib/chart/dim.js +13 -0
- package/dist/lib/chart/format.js +7 -1
- package/dist/lib/chart/motion.js +6 -4
- package/dist/lib/highlight-glide.js +68 -10
- package/dist/lib/taxonomy.js +5 -0
- package/dist/motion/Burst.js +79 -0
- package/dist/motion/MotionProvider.js +45 -3
- package/dist/motion/Reveal.js +1 -1
- package/dist/motion/TextReveal.js +1 -1
- package/dist/motion/index.js +17 -2
- package/dist/motion/useAutoSize.js +42 -0
- package/dist/motion/useShake.js +25 -0
- package/dist/styles.css +439 -5
- package/llms/blocks/approval-block.md +128 -0
- package/llms/blocks/message-reactions-block.md +116 -0
- package/llms/components/accordion.md +3 -2
- package/llms/components/area-chart.md +4 -3
- package/llms/components/attachment.md +7 -0
- package/llms/components/bar-chart.md +5 -4
- package/llms/components/bottom-nav.md +8 -0
- package/llms/components/breadcrumbs.md +7 -0
- package/llms/components/bubble.md +17 -7
- package/llms/components/button.md +6 -6
- package/llms/components/calendar.md +9 -0
- package/llms/components/carousel.md +17 -0
- package/llms/components/chat-composer.md +9 -0
- package/llms/components/checkbox-card.md +8 -0
- package/llms/components/checkbox.md +8 -0
- package/llms/components/code-block.md +8 -0
- package/llms/components/collapsible.md +8 -0
- package/llms/components/color-field.md +7 -0
- package/llms/components/combobox.md +10 -0
- package/llms/components/command-menu.md +9 -0
- package/llms/components/confirm-button.md +92 -0
- package/llms/components/data-table.md +8 -0
- package/llms/components/entity.md +7 -0
- package/llms/components/feedback.md +23 -3
- package/llms/components/input-group.md +8 -1
- package/llms/components/like-button.md +91 -0
- package/llms/components/line-chart.md +4 -3
- package/llms/components/middle-truncate.md +1 -1
- package/llms/components/note.md +8 -0
- package/llms/components/notification-badge.md +10 -2
- package/llms/components/number-ticker.md +86 -0
- package/llms/components/otp-field.md +15 -1
- package/llms/components/panel-group.md +7 -0
- package/llms/components/panel-section.md +8 -0
- package/llms/components/pie-chart.md +6 -4
- package/llms/components/pill.md +8 -0
- package/llms/components/radar-chart.md +4 -3
- package/llms/components/radial-chart.md +6 -4
- package/llms/components/radio-card.md +9 -0
- package/llms/components/radio.md +7 -0
- package/llms/components/reaction-picker.md +115 -0
- package/llms/components/relative-time.md +8 -0
- package/llms/components/scroller.md +8 -0
- package/llms/components/scrub-field.md +8 -0
- package/llms/components/search-input.md +7 -0
- package/llms/components/show-more.md +9 -0
- package/llms/components/side-nav.md +8 -0
- package/llms/components/sidebar.md +9 -0
- package/llms/components/slider.md +7 -0
- package/llms/components/sparkline.md +3 -2
- package/llms/components/table.md +7 -0
- package/llms/components/tabs.md +20 -2
- package/llms/components/theme-selector.md +8 -0
- package/llms/components/theme-switcher.md +17 -0
- package/llms/components/toast.md +2 -1
- package/llms/components/toggle.md +19 -2
- package/llms/components/tool-call.md +115 -0
- package/llms/components/tooltip.md +11 -1
- package/llms/components/tree.md +8 -0
- package/llms/components/video.md +8 -0
- package/llms/foundations/motion.md +64 -3
- package/llms/reference/utilities.md +26 -1
- package/llms/tasks/reviewing-motion.md +14 -0
- package/llms-full.txt +1337 -69
- package/llms.txt +19 -11
- package/package.json +1 -1
- package/registry.json +3040 -124
- 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
|
-
|
|
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__ */
|
|
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 {
|
|
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
|
|
14
|
-
const
|
|
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 = !
|
|
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:
|
|
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:
|
|
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 {
|
|
7
|
-
|
|
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
|
-
|
|
10
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
208
|
-
success: "
|
|
209
|
-
error: "
|
|
210
|
-
warning: "
|
|
211
|
-
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
|
|
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
|
-
) :
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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,
|