@streamsai/ui 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +57 -2
- package/dist/components/ui/agent-avatar.d.ts +50 -0
- package/dist/components/ui/agent-avatar.js +38 -0
- package/dist/components/ui/alert-dialog.d.ts +12 -0
- package/dist/components/ui/alert-dialog.js +37 -0
- package/dist/components/ui/alert.d.ts +10 -0
- package/dist/components/ui/alert.js +28 -0
- package/dist/components/ui/assets/loader.json.js +1 -0
- package/dist/components/ui/avatar-frame.d.ts +11 -0
- package/dist/components/ui/avatar-frame.js +20 -0
- package/dist/components/ui/avatar.d.ts +23 -0
- package/dist/components/ui/avatar.js +54 -0
- package/dist/components/ui/badge.d.ts +7 -0
- package/dist/components/ui/badge.js +36 -0
- package/dist/components/ui/bar-list.d.ts +40 -0
- package/dist/components/ui/bar-list.js +37 -0
- package/dist/components/ui/breadcrumb.d.ts +8 -0
- package/dist/components/ui/breadcrumb.js +21 -0
- package/dist/components/ui/button-group.d.ts +10 -0
- package/dist/components/ui/button-group.js +41 -0
- package/dist/components/ui/button.d.ts +9 -0
- package/dist/components/ui/button.js +39 -0
- package/dist/components/ui/card.d.ts +9 -0
- package/dist/components/ui/card.js +25 -0
- package/dist/components/ui/checkbox.d.ts +3 -0
- package/dist/components/ui/checkbox.js +16 -0
- package/dist/components/ui/chip.d.ts +13 -0
- package/dist/components/ui/chip.js +22 -0
- package/dist/components/ui/clickable.d.ts +27 -0
- package/dist/components/ui/clickable.js +54 -0
- package/dist/components/ui/code-block.d.ts +68 -0
- package/dist/components/ui/code-block.js +112 -0
- package/dist/components/ui/code-view.d.ts +21 -0
- package/dist/components/ui/code-view.js +20 -0
- package/dist/components/ui/collapsible.d.ts +6 -0
- package/dist/components/ui/collapsible.js +18 -0
- package/dist/components/ui/combobox.d.ts +14 -0
- package/dist/components/ui/combobox.js +37 -0
- package/dist/components/ui/command.d.ts +15 -0
- package/dist/components/ui/command.js +34 -0
- package/dist/components/ui/context-menu.d.ts +21 -0
- package/dist/components/ui/context-menu.js +60 -0
- package/dist/components/ui/counter.d.ts +11 -0
- package/dist/components/ui/counter.js +20 -0
- package/dist/components/ui/dialog.d.ts +17 -0
- package/dist/components/ui/dialog.js +41 -0
- package/dist/components/ui/disabled-reason.d.ts +22 -0
- package/dist/components/ui/disabled-reason.js +16 -0
- package/dist/components/ui/dropdown-menu.d.ts +33 -0
- package/dist/components/ui/dropdown-menu.js +58 -0
- package/dist/components/ui/editable-text.d.ts +95 -0
- package/dist/components/ui/editable-text.js +191 -0
- package/dist/components/ui/heading.d.ts +24 -0
- package/dist/components/ui/heading.js +92 -0
- package/dist/components/ui/hover-card.d.ts +9 -0
- package/dist/components/ui/hover-card.js +16 -0
- package/dist/components/ui/icon-button.d.ts +23 -0
- package/dist/components/ui/icon-button.js +32 -0
- package/dist/components/ui/input.d.ts +3 -0
- package/dist/components/ui/input.js +8 -0
- package/dist/components/ui/kbd.d.ts +12 -0
- package/dist/components/ui/kbd.js +12 -0
- package/dist/components/ui/label.d.ts +3 -0
- package/dist/components/ui/label.js +7 -0
- package/dist/components/ui/loader.d.ts +19 -0
- package/dist/components/ui/loader.js +157 -0
- package/dist/components/ui/message-toolbar.d.ts +20 -0
- package/dist/components/ui/message-toolbar.js +17 -0
- package/dist/components/ui/modal.d.ts +30 -0
- package/dist/components/ui/modal.js +41 -0
- package/dist/components/ui/nav-item.d.ts +25 -0
- package/dist/components/ui/nav-item.js +66 -0
- package/dist/components/ui/panel-section.d.ts +22 -0
- package/dist/components/ui/panel-section.js +26 -0
- package/dist/components/ui/popover.d.ts +14 -0
- package/dist/components/ui/popover.js +30 -0
- package/dist/components/ui/progress.d.ts +5 -0
- package/dist/components/ui/progress.js +16 -0
- package/dist/components/ui/provider-badge.d.ts +22 -0
- package/dist/components/ui/provider-badge.js +38 -0
- package/dist/components/ui/radio-group.d.ts +5 -0
- package/dist/components/ui/radio-group.js +21 -0
- package/dist/components/ui/reply-summary.d.ts +16 -0
- package/dist/components/ui/reply-summary.js +8 -0
- package/dist/components/ui/resizable.d.ts +8 -0
- package/dist/components/ui/resizable.js +15 -0
- package/dist/components/ui/rich-text-editor.d.ts +447 -0
- package/dist/components/ui/rich-text-editor.js +2419 -0
- package/dist/components/ui/running-indicator.d.ts +17 -0
- package/dist/components/ui/running-indicator.js +34 -0
- package/dist/components/ui/sandboxed-frame.d.ts +29 -0
- package/dist/components/ui/sandboxed-frame.js +77 -0
- package/dist/components/ui/scroll-area.d.ts +14 -0
- package/dist/components/ui/scroll-area.js +17 -0
- package/dist/components/ui/section-header.d.ts +12 -0
- package/dist/components/ui/section-header.js +12 -0
- package/dist/components/ui/select.d.ts +15 -0
- package/dist/components/ui/select.js +40 -0
- package/dist/components/ui/separator.d.ts +3 -0
- package/dist/components/ui/separator.js +13 -0
- package/dist/components/ui/sheet.d.ts +23 -0
- package/dist/components/ui/sheet.js +48 -0
- package/dist/components/ui/shimmer-text.d.ts +26 -0
- package/dist/components/ui/shimmer-text.js +32 -0
- package/dist/components/ui/sidebar.d.ts +63 -0
- package/dist/components/ui/sidebar.js +283 -0
- package/dist/components/ui/skeleton.d.ts +14 -0
- package/dist/components/ui/skeleton.js +12 -0
- package/dist/components/ui/sonner.d.ts +4 -0
- package/dist/components/ui/sonner.js +14 -0
- package/dist/components/ui/spark-bars.d.ts +29 -0
- package/dist/components/ui/spark-bars.js +24 -0
- package/dist/components/ui/spinner.d.ts +16 -0
- package/dist/components/ui/spinner.js +26 -0
- package/dist/components/ui/stacked-bar.d.ts +37 -0
- package/dist/components/ui/stacked-bar.js +56 -0
- package/dist/components/ui/status-dot.d.ts +16 -0
- package/dist/components/ui/status-dot.js +32 -0
- package/dist/components/ui/switch.d.ts +5 -0
- package/dist/components/ui/switch.js +13 -0
- package/dist/components/ui/tabs.d.ts +14 -0
- package/dist/components/ui/tabs.js +55 -0
- package/dist/components/ui/text.d.ts +27 -0
- package/dist/components/ui/text.js +100 -0
- package/dist/components/ui/textarea.d.ts +3 -0
- package/dist/components/ui/textarea.js +7 -0
- package/dist/components/ui/theme-toggle.d.ts +21 -0
- package/dist/components/ui/theme-toggle.js +17 -0
- package/dist/components/ui/toggle-group.d.ts +6 -0
- package/dist/components/ui/toggle-group.js +12 -0
- package/dist/components/ui/tooltip.d.ts +11 -0
- package/dist/components/ui/tooltip.js +21 -0
- package/dist/components/ui/trace.d.ts +24 -0
- package/dist/components/ui/trace.js +57 -0
- package/dist/components/ui/trend-line.d.ts +93 -0
- package/dist/components/ui/trend-line.js +215 -0
- package/dist/components/ui/truncate.d.ts +30 -0
- package/dist/components/ui/truncate.js +54 -0
- package/dist/components/ui/viewer-shell.d.ts +55 -0
- package/dist/components/ui/viewer-shell.js +15 -0
- package/dist/components/ui/virtual-list.d.ts +91 -0
- package/dist/components/ui/virtual-list.js +406 -0
- package/dist/components/ui/visually-hidden.d.ts +15 -0
- package/dist/components/ui/visually-hidden.js +19 -0
- package/dist/direction/index.d.ts +19 -0
- package/dist/direction/index.js +32 -0
- package/dist/hooks/use-code-highlighting.d.ts +22 -0
- package/dist/hooks/use-code-highlighting.js +47 -0
- package/dist/hooks/use-is-truncated.d.ts +7 -0
- package/dist/hooks/use-is-truncated.js +23 -0
- package/dist/hooks/use-mobile.d.ts +2 -0
- package/dist/hooks/use-mobile.js +15 -0
- package/dist/icons/custom.d.ts +29 -0
- package/dist/icons/custom.js +48 -0
- package/dist/icons/index.d.ts +49 -0
- package/dist/icons/index.js +50 -0
- package/dist/icons/provider-logos.d.ts +17 -0
- package/dist/icons/provider-logos.js +28 -0
- package/dist/icons/providers.d.ts +9 -0
- package/dist/icons/providers.js +51 -0
- package/dist/index.d.ts +102 -0
- package/dist/index.js +124 -0
- package/dist/lib/code-highlighter.d.ts +9 -0
- package/dist/lib/code-highlighter.js +65 -0
- package/dist/lib/code-language.d.ts +13 -0
- package/dist/lib/code-language.js +103 -0
- package/dist/lib/code-tokens.d.ts +9 -0
- package/dist/lib/code-tokens.js +27 -0
- package/dist/lib/copy-text.d.ts +6 -0
- package/dist/lib/copy-text.js +46 -0
- package/dist/lib/home-tile.d.ts +12 -0
- package/dist/lib/home-tile.js +45 -0
- package/dist/lib/reveal-mask.d.ts +9 -0
- package/dist/lib/reveal-mask.js +16 -0
- package/dist/lib/text-direction.d.ts +28 -0
- package/dist/lib/text-direction.js +119 -0
- package/dist/lib/utils.d.ts +11 -0
- package/dist/lib/utils.js +30 -0
- package/dist/motion/components.d.ts +184 -0
- package/dist/motion/components.js +211 -0
- package/dist/motion/config.d.ts +849 -0
- package/dist/motion/config.js +429 -0
- package/dist/motion/index.d.ts +8 -0
- package/dist/motion/index.js +8 -0
- package/dist/motion/modal-presence.d.ts +5 -0
- package/dist/motion/modal-presence.js +27 -0
- package/dist/motion/popup.d.ts +77 -0
- package/dist/motion/popup.js +103 -0
- package/dist/platform/index.d.ts +29 -0
- package/dist/platform/index.js +63 -0
- package/dist/styles/base.css +319 -0
- package/dist/styles/fonts/Figtree-Variable.woff2 +0 -0
- package/dist/styles/fonts/OFL.txt +93 -0
- package/dist/styles/fonts/Poppins-Medium.woff2 +0 -0
- package/dist/styles/fonts/Poppins-Regular.woff2 +0 -0
- package/dist/styles/fonts/Poppins-SemiBold.woff2 +0 -0
- package/dist/styles/fonts.css +28 -0
- package/dist/styles/globals.css +8 -0
- package/dist/styles/mcp-host.css +76 -0
- package/dist/styles/tokens.css +387 -0
- package/dist/styles/tokens.d.mts +57 -0
- package/dist/styles/tokens.mjs +59 -0
- package/dist/theme/index.d.ts +28 -0
- package/dist/theme/index.js +65 -0
- package/package.json +136 -3
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
/** Seconds (what `motion` takes). tokens.css carries the same numbers in ms. */
|
|
2
|
+
export const duration = {
|
|
3
|
+
/** Sense `short-100`: a small element leaving (`presets.scaleIn`'s exit). */
|
|
4
|
+
short: 0.1,
|
|
5
|
+
/** Hover washes, icon swaps, press feedback: anything the pointer is already on. */
|
|
6
|
+
fast: 0.12,
|
|
7
|
+
/** The default: popups, panels, rows entering, route content, collapses. */
|
|
8
|
+
base: 0.2,
|
|
9
|
+
/** Large surfaces travelling a long way: sheets, the details pane, the viewer. */
|
|
10
|
+
slow: 0.32,
|
|
11
|
+
/** One orchestrated page-load moment (brand and auth pages, `presets.reveal`). */
|
|
12
|
+
reveal: 0.48,
|
|
13
|
+
};
|
|
14
|
+
export const ease = {
|
|
15
|
+
/** Symmetric, for things that change in place (colour, width, rotation). */
|
|
16
|
+
standard: [0.4, 0, 0.2, 1],
|
|
17
|
+
/** Entrances: fast start, long soft landing. */
|
|
18
|
+
out: [0.16, 1, 0.3, 1],
|
|
19
|
+
/** Exits: leave quickly, no lingering. */
|
|
20
|
+
in: [0.4, 0, 1, 1],
|
|
21
|
+
/** Sense `ease-enter`: decelerates into place, for scale entrances. */
|
|
22
|
+
enter: [0, 0, 0.2, 1],
|
|
23
|
+
/** Sense `ease-exit`: accelerates away, for scale exits. */
|
|
24
|
+
exit: [0.6, 0, 1, 1],
|
|
25
|
+
};
|
|
26
|
+
/** Springs for things that move because the layout moved (reorders, pills, indicators). */
|
|
27
|
+
export const spring = {
|
|
28
|
+
/** Layout shifts, reorders, a tab indicator sliding: settles in about `base`. */
|
|
29
|
+
snappy: { type: 'spring', visualDuration: 0.22, bounce: 0.12 },
|
|
30
|
+
/** Small things popping into place (reactions, badges): a touch of overshoot. */
|
|
31
|
+
pop: { type: 'spring', visualDuration: 0.24, bounce: 0.28 },
|
|
32
|
+
};
|
|
33
|
+
/** Pixels of travel. Motion here suggests direction, it never carries content across the screen. */
|
|
34
|
+
export const distance = {
|
|
35
|
+
/** Rows, route content, toasts: the default rise. */
|
|
36
|
+
nudge: 4,
|
|
37
|
+
/** Cards and empty states arriving on their own. */
|
|
38
|
+
rise: 8,
|
|
39
|
+
/** Panels and drawers sliding in from an edge. */
|
|
40
|
+
slide: 16,
|
|
41
|
+
};
|
|
42
|
+
/** Scale a popping element starts from. */
|
|
43
|
+
export const scale = {
|
|
44
|
+
pop: 0.94,
|
|
45
|
+
/** Popups and dialogs growing out of their trigger. */
|
|
46
|
+
popup: 0.96,
|
|
47
|
+
subtle: 0.98,
|
|
48
|
+
/** Sense `scale-enter` / `scale-exit`: never from 0, always from 95%. */
|
|
49
|
+
enter: 0.95,
|
|
50
|
+
};
|
|
51
|
+
/** Seconds between siblings in a staggered group, and the ceiling on the whole cascade. */
|
|
52
|
+
export const stagger = {
|
|
53
|
+
step: 0.03,
|
|
54
|
+
/** Sense `short-20`: the offset between peers entering together, and an exit's fade delay. */
|
|
55
|
+
offset: 0.02,
|
|
56
|
+
/** Items beyond this delay arrive together, so a long list never takes long to appear. */
|
|
57
|
+
max: 0.18,
|
|
58
|
+
};
|
|
59
|
+
/** Ready-made transitions for the common cases. */
|
|
60
|
+
export const transitions = {
|
|
61
|
+
fast: { duration: duration.fast, ease: ease.standard },
|
|
62
|
+
base: { duration: duration.base, ease: ease.standard },
|
|
63
|
+
enter: { duration: duration.base, ease: ease.out },
|
|
64
|
+
/** Popups, menus and tooltips: they answer a click, so they arrive at `fast`. */
|
|
65
|
+
enterFast: { duration: duration.fast, ease: ease.out },
|
|
66
|
+
exit: { duration: duration.fast, ease: ease.in },
|
|
67
|
+
slow: { duration: duration.slow, ease: ease.out },
|
|
68
|
+
/** A meter that follows a clock ticking every `base`: each step is linear so the sum drains evenly. */
|
|
69
|
+
meter: { duration: duration.base, ease: 'linear' },
|
|
70
|
+
layout: spring.snappy,
|
|
71
|
+
pop: spring.pop,
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Pointer / tap press feedback. Use on a `motion.*` element as
|
|
75
|
+
* `whileTap={press.whileTap}` (and `transition={press.transition}`), never CSS
|
|
76
|
+
* `active:scale-*`. Reduced motion is honoured by `MotionProvider`.
|
|
77
|
+
*
|
|
78
|
+
* `commit.scale` stays a mutable `number[]` so it is assignable to motion's
|
|
79
|
+
* `animate` keyframes (a `readonly` tuple from `as const` is not).
|
|
80
|
+
*/
|
|
81
|
+
export const press = {
|
|
82
|
+
/** Scale while the pointer is down — quiet shrink, not a bounce. */
|
|
83
|
+
scale: scale.pop,
|
|
84
|
+
whileTap: { scale: scale.pop },
|
|
85
|
+
transition: transitions.fast,
|
|
86
|
+
/**
|
|
87
|
+
* One-shot shrink→grow for keyboard / programmatic commit (e.g. Enter-to-send)
|
|
88
|
+
* where `whileTap` never fires. Drive `animate` from a short-lived flag.
|
|
89
|
+
*/
|
|
90
|
+
commit: {
|
|
91
|
+
scale: [1, scale.pop, 1],
|
|
92
|
+
transition: spring.pop,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Prop bags for `motion.*` elements: `<motion.div {...presets.rise} />`. Each carries its own
|
|
97
|
+
* enter and exit, so it behaves the same inside `AnimatePresence` or on its own. Reach for these
|
|
98
|
+
* before writing an inline `initial` / `animate`; add a preset here rather than tuning one call site.
|
|
99
|
+
*/
|
|
100
|
+
export const presets = {
|
|
101
|
+
/** Opacity only. Overlays, content swapping in place, anything that should not move. */
|
|
102
|
+
fade: {
|
|
103
|
+
initial: { opacity: 0 },
|
|
104
|
+
animate: { opacity: 1, transition: transitions.enter },
|
|
105
|
+
exit: { opacity: 0, transition: transitions.exit },
|
|
106
|
+
},
|
|
107
|
+
/** The default entrance: fade plus a small rise. Rows, route content, messages. */
|
|
108
|
+
rise: {
|
|
109
|
+
initial: { opacity: 0, y: distance.nudge },
|
|
110
|
+
animate: { opacity: 1, y: 0, transition: transitions.enter },
|
|
111
|
+
exit: { opacity: 0, y: -distance.nudge / 2, transition: transitions.exit },
|
|
112
|
+
},
|
|
113
|
+
/** A card or empty state arriving on its own, a little more travel than `rise`. */
|
|
114
|
+
lift: {
|
|
115
|
+
initial: { opacity: 0, y: distance.rise },
|
|
116
|
+
animate: { opacity: 1, y: 0, transition: transitions.slow },
|
|
117
|
+
exit: { opacity: 0, y: distance.nudge, transition: transitions.exit },
|
|
118
|
+
},
|
|
119
|
+
/** Small elements appearing (reaction pills, badges, counters, indicators). */
|
|
120
|
+
pop: {
|
|
121
|
+
initial: { opacity: 0, scale: scale.pop },
|
|
122
|
+
animate: { opacity: 1, scale: 1, transition: transitions.pop },
|
|
123
|
+
exit: { opacity: 0, scale: scale.pop, transition: transitions.exit },
|
|
124
|
+
},
|
|
125
|
+
/** One piece of content replacing another in the same spot (Copy → Copied, icon swaps). */
|
|
126
|
+
swap: {
|
|
127
|
+
initial: { opacity: 0, scale: scale.subtle, filter: 'blur(2px)' },
|
|
128
|
+
// `transitionEnd` drops the settled filter, so resting text is not left on a filtered layer.
|
|
129
|
+
animate: {
|
|
130
|
+
opacity: 1,
|
|
131
|
+
scale: 1,
|
|
132
|
+
filter: 'blur(0px)',
|
|
133
|
+
transition: transitions.fast,
|
|
134
|
+
transitionEnd: { filter: 'none' },
|
|
135
|
+
},
|
|
136
|
+
exit: { opacity: 0, scale: scale.subtle, filter: 'blur(2px)', transition: transitions.exit },
|
|
137
|
+
},
|
|
138
|
+
/**
|
|
139
|
+
* Odometer roll for a rising count: new value enters from the top, old exits downward.
|
|
140
|
+
* Clip the parent (`overflow-hidden`) so the travel stays inside the glyph box.
|
|
141
|
+
*/
|
|
142
|
+
wheelUp: {
|
|
143
|
+
initial: { y: '-100%', opacity: 0 },
|
|
144
|
+
animate: { y: 0, opacity: 1, transition: transitions.fast },
|
|
145
|
+
exit: { y: '100%', opacity: 0, transition: { duration: duration.short, ease: ease.in } },
|
|
146
|
+
},
|
|
147
|
+
/** Reverse of `wheelUp` — a falling count rolls the other way. */
|
|
148
|
+
wheelDown: {
|
|
149
|
+
initial: { y: '100%', opacity: 0 },
|
|
150
|
+
animate: { y: 0, opacity: 1, transition: transitions.fast },
|
|
151
|
+
exit: { y: '-100%', opacity: 0, transition: { duration: duration.short, ease: ease.in } },
|
|
152
|
+
},
|
|
153
|
+
/** A dialog arriving over its backdrop: a fade, a slight grow and a short rise. */
|
|
154
|
+
dialog: {
|
|
155
|
+
initial: { opacity: 0, scale: scale.popup, y: distance.nudge },
|
|
156
|
+
animate: { opacity: 1, scale: 1, y: 0, transition: transitions.enter },
|
|
157
|
+
exit: { opacity: 0, scale: scale.subtle, transition: transitions.exit },
|
|
158
|
+
},
|
|
159
|
+
/** One-off page-load moment for brand surfaces (sign-in, the landing hero). */
|
|
160
|
+
reveal: {
|
|
161
|
+
initial: { opacity: 0, y: distance.rise, filter: 'blur(4px)' },
|
|
162
|
+
animate: {
|
|
163
|
+
opacity: 1,
|
|
164
|
+
y: 0,
|
|
165
|
+
filter: 'blur(0px)',
|
|
166
|
+
transition: { duration: duration.reveal, ease: ease.out },
|
|
167
|
+
transitionEnd: { filter: 'none' },
|
|
168
|
+
},
|
|
169
|
+
exit: { opacity: 0, transition: transitions.exit },
|
|
170
|
+
},
|
|
171
|
+
/** Height 0 ↔ auto, for disclosure content and rows that expand in place. */
|
|
172
|
+
collapse: {
|
|
173
|
+
initial: { height: 0, opacity: 0 },
|
|
174
|
+
animate: {
|
|
175
|
+
height: 'auto',
|
|
176
|
+
opacity: 1,
|
|
177
|
+
transition: { height: transitions.enter, opacity: { ...transitions.enter, delay: 0.04 } },
|
|
178
|
+
},
|
|
179
|
+
exit: {
|
|
180
|
+
height: 0,
|
|
181
|
+
opacity: 0,
|
|
182
|
+
transition: { height: transitions.base, opacity: transitions.exit },
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
/**
|
|
186
|
+
* Width 0 ↔ auto, for side panels that take space from their neighbour (the details pane): the
|
|
187
|
+
* neighbour narrows with the panel instead of snapping to its final width in one frame. The
|
|
188
|
+
* panel is an opaque surface, so it slides in solid with its edge. A fade on its own clock made
|
|
189
|
+
* the neighbour move while the panel was still invisible (and after it had gone), which read as
|
|
190
|
+
* two animations instead of one.
|
|
191
|
+
*/
|
|
192
|
+
collapseX: {
|
|
193
|
+
initial: { width: 0 },
|
|
194
|
+
animate: { width: 'auto', transition: transitions.enter },
|
|
195
|
+
exit: { width: 0, transition: transitions.base },
|
|
196
|
+
},
|
|
197
|
+
/**
|
|
198
|
+
* Sense `scale-enter` / `scale-exit`: grows from 95% on `ease.enter` with a linear fade over
|
|
199
|
+
* `base`, and leaves faster (`short`) on `ease.exit` with its fade a beat behind. Chips, thumbnails
|
|
200
|
+
* and cards that appear in place without travelling.
|
|
201
|
+
*/
|
|
202
|
+
scaleIn: {
|
|
203
|
+
initial: { opacity: 0, scale: scale.enter },
|
|
204
|
+
animate: {
|
|
205
|
+
opacity: 1,
|
|
206
|
+
scale: 1,
|
|
207
|
+
transition: {
|
|
208
|
+
scale: { duration: duration.base, ease: ease.enter },
|
|
209
|
+
opacity: { duration: duration.base, ease: 'linear' },
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
exit: {
|
|
213
|
+
opacity: 0,
|
|
214
|
+
scale: scale.enter,
|
|
215
|
+
transition: {
|
|
216
|
+
scale: { duration: duration.short, ease: ease.exit },
|
|
217
|
+
opacity: { duration: duration.short, ease: 'linear', delay: stagger.offset },
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
/**
|
|
223
|
+
* Menus, popovers, selects, tooltips and hover cards: a quick scale-and-fade that drifts a few
|
|
224
|
+
* pixels out from the trigger. `side` is where the popup sits relative to its trigger.
|
|
225
|
+
*/
|
|
226
|
+
export const popup = (side = 'bottom', dir = 'ltr') => {
|
|
227
|
+
const physical = side === 'inline-start'
|
|
228
|
+
? dir === 'rtl'
|
|
229
|
+
? 'right'
|
|
230
|
+
: 'left'
|
|
231
|
+
: side === 'inline-end'
|
|
232
|
+
? dir === 'rtl'
|
|
233
|
+
? 'left'
|
|
234
|
+
: 'right'
|
|
235
|
+
: side;
|
|
236
|
+
const n = distance.nudge;
|
|
237
|
+
const from = { top: { y: n }, bottom: { y: -n }, left: { x: n }, right: { x: -n }, none: {} }[physical];
|
|
238
|
+
return {
|
|
239
|
+
initial: { opacity: 0, scale: scale.popup, x: 0, y: 0, ...from },
|
|
240
|
+
animate: { opacity: 1, scale: 1, x: 0, y: 0, transition: transitions.enterFast },
|
|
241
|
+
exit: { opacity: 0, scale: scale.popup, transition: transitions.exit },
|
|
242
|
+
};
|
|
243
|
+
};
|
|
244
|
+
/**
|
|
245
|
+
* A popup that fades and drifts a few pixels from the side it opens on, without the scale of
|
|
246
|
+
* `popup`. For a surface that reads as part of the form it belongs to: the dropdown lists and
|
|
247
|
+
* pickers of a dense editor. `tip` arrives faster than a list; `fade` does not move at all (a
|
|
248
|
+
* list that opens on hover).
|
|
249
|
+
*/
|
|
250
|
+
export const drift = (side = 'bottom', kind = 'list') => {
|
|
251
|
+
const seconds = kind === 'tip' ? duration.short : kind === 'fade' ? duration.fast : 0.18;
|
|
252
|
+
const from = kind === 'fade' ? 0 : side === 'top' ? distance.nudge : side === 'bottom' ? -distance.nudge : 0;
|
|
253
|
+
// The CSS `ease` curve, which the design's fade-and-rise is specified with.
|
|
254
|
+
const transition = { duration: seconds, ease: [0.25, 0.1, 0.25, 1] };
|
|
255
|
+
return {
|
|
256
|
+
initial: { opacity: 0, y: from },
|
|
257
|
+
animate: { opacity: 1, y: 0, transition },
|
|
258
|
+
exit: { opacity: 0, y: from, transition },
|
|
259
|
+
};
|
|
260
|
+
};
|
|
261
|
+
/** Sheets and drawers: the whole panel travels in from its edge and back out. */
|
|
262
|
+
export const sheet = (side) => {
|
|
263
|
+
const off = {
|
|
264
|
+
top: { y: '-100%' },
|
|
265
|
+
bottom: { y: '100%' },
|
|
266
|
+
left: { x: '-100%' },
|
|
267
|
+
right: { x: '100%' },
|
|
268
|
+
}[side];
|
|
269
|
+
const home = 'x' in off ? { x: 0 } : { y: 0 };
|
|
270
|
+
return {
|
|
271
|
+
initial: off,
|
|
272
|
+
animate: { ...home, transition: transitions.slow },
|
|
273
|
+
exit: { ...off, transition: { duration: duration.base, ease: ease.in } },
|
|
274
|
+
};
|
|
275
|
+
};
|
|
276
|
+
/** Inline-start / end slides for panels, drawers and pane swaps. `dir` flips them in RTL. */
|
|
277
|
+
export const slide = (from, dir = 'ltr') => {
|
|
278
|
+
const sign = (from === 'end' ? 1 : -1) * (dir === 'rtl' ? -1 : 1);
|
|
279
|
+
return {
|
|
280
|
+
initial: { opacity: 0, x: sign * distance.slide },
|
|
281
|
+
animate: { opacity: 1, x: 0, transition: transitions.enter },
|
|
282
|
+
exit: { opacity: 0, x: sign * distance.slide, transition: transitions.exit },
|
|
283
|
+
};
|
|
284
|
+
};
|
|
285
|
+
/**
|
|
286
|
+
* Height changes of content that stays in place, driven by `AutoHeight`. Symmetric, because a card
|
|
287
|
+
* that grows and shrinks should not snap on either leg (`ease.out` front-loads nearly all of the
|
|
288
|
+
* movement into the first frames). `reveal` is for small things appearing in a card (an error, an
|
|
289
|
+
* input, a list expanding); `step` is for a whole question changing, so it matches `stepSwap`.
|
|
290
|
+
*/
|
|
291
|
+
export const resize = {
|
|
292
|
+
reveal: { duration: 0.25, ease: ease.standard },
|
|
293
|
+
step: { duration: 0.35, ease: ease.standard },
|
|
294
|
+
};
|
|
295
|
+
/**
|
|
296
|
+
* A diagram re-flowing around a change (a branch added or removed in the cycle builder): nodes,
|
|
297
|
+
* connectors, chips and the line joining them all travel on this one clock, so they arrive together
|
|
298
|
+
* and nothing is seen jumping. Slightly slower than a card moving alone, because the whole picture moves.
|
|
299
|
+
*/
|
|
300
|
+
export const reflow = { duration: 0.3, ease: ease.standard };
|
|
301
|
+
/**
|
|
302
|
+
* A row gliding into a list that follows its end (a cycle divider): the rows above rise as it grows,
|
|
303
|
+
* so it is deliberately slower than `reflow`, and symmetric so the lift starts and lands softly.
|
|
304
|
+
*/
|
|
305
|
+
export const glide = { duration: 0.65, ease: ease.standard };
|
|
306
|
+
/**
|
|
307
|
+
* The edit button of a connector sliding out from behind its "+" (cycle builder). `shift` is the
|
|
308
|
+
* button's width plus the 6px gap. The slide is quick and decelerates hard; the fade is `fast`.
|
|
309
|
+
* Reduced motion drops the transform and keeps the fade (`MotionProvider`).
|
|
310
|
+
*/
|
|
311
|
+
export const edgeReveal = {
|
|
312
|
+
shift: 42,
|
|
313
|
+
transition: {
|
|
314
|
+
x: { duration: 0.18, ease: [0.2, 0, 0, 1] },
|
|
315
|
+
opacity: { duration: duration.fast, ease: ease.standard },
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
/**
|
|
319
|
+
* One step of a multi-step flow replacing another, cross-fading (use with `AnimatePresence
|
|
320
|
+
* mode="popLayout" custom={dir}`). `dir` is 1 for forward and -1 for back: the old step leaves
|
|
321
|
+
* toward the side it came from and the new one arrives a beat later from the other, so the two
|
|
322
|
+
* overlap and nothing pops. Travel is `distance.slide`.
|
|
323
|
+
*/
|
|
324
|
+
export const stepSwap = {
|
|
325
|
+
initial: (dir) => ({ opacity: 0, x: dir * distance.slide }),
|
|
326
|
+
animate: {
|
|
327
|
+
opacity: 1,
|
|
328
|
+
x: 0,
|
|
329
|
+
transition: { duration: 0.3, delay: stagger.step + stagger.offset, ease: ease.standard },
|
|
330
|
+
},
|
|
331
|
+
exit: (dir) => ({
|
|
332
|
+
opacity: 0,
|
|
333
|
+
x: -dir * distance.slide,
|
|
334
|
+
transition: { duration: 0.2, ease: ease.standard },
|
|
335
|
+
}),
|
|
336
|
+
};
|
|
337
|
+
/**
|
|
338
|
+
* A clipped one-line label (sidebar stream and session titles) sliding the rest of itself into
|
|
339
|
+
* view while hovered, then holding at the end. `to(px)` runs at a steady reading pace, so a long
|
|
340
|
+
* title takes longer than a short one, after a rest that keeps a pointer sweeping past rows from
|
|
341
|
+
* setting them all moving. `back` snaps home at once when the pointer leaves: no rewind.
|
|
342
|
+
*/
|
|
343
|
+
export const scrollReveal = {
|
|
344
|
+
/** Seconds the pointer rests on the row before the title starts moving. */
|
|
345
|
+
delay: 0.3,
|
|
346
|
+
/** Pixels per second. */
|
|
347
|
+
speed: 48,
|
|
348
|
+
to: (px) => ({
|
|
349
|
+
duration: px / scrollReveal.speed,
|
|
350
|
+
delay: scrollReveal.delay,
|
|
351
|
+
ease: 'linear',
|
|
352
|
+
}),
|
|
353
|
+
back: { duration: 0 },
|
|
354
|
+
};
|
|
355
|
+
/** Delay for the `index`-th item of a staggered group, `step` apart and capped at `stagger.max`. */
|
|
356
|
+
export const staggerDelay = (index, step = stagger.step) => Math.min(index * step, stagger.max);
|
|
357
|
+
/**
|
|
358
|
+
* Infinite-loop transitions for loading indicators. These run forever at a steady cadence so the
|
|
359
|
+
* loading state is always perceptible. Under reduced motion anything that moves stops (the spinner
|
|
360
|
+
* stops turning) and the opacity pulses carry on, so loading still shows.
|
|
361
|
+
*/
|
|
362
|
+
export const loops = {
|
|
363
|
+
/** Spinner: one full clockwise turn per second, linear. Animate `transform`, not `rotate`, so
|
|
364
|
+
* the browser runs it (motion drives `rotate` in JS, every frame). */
|
|
365
|
+
spin: { repeat: Infinity, repeatType: 'loop', ease: 'linear', duration: 1 },
|
|
366
|
+
/**
|
|
367
|
+
* Skeleton and status-dot: smooth opacity float, slow enough to read as "still loading". Give it
|
|
368
|
+
* keyframes that come back to where they start (`[1, 0.4, 1]`): it loops rather than mirrors, so
|
|
369
|
+
* the browser runs it off the main thread (motion drives a mirrored loop in JS, every frame).
|
|
370
|
+
*/
|
|
371
|
+
pulse: { repeat: Infinity, repeatType: 'loop', ease: ease.standard, duration: 1.4 },
|
|
372
|
+
/** Text caret while an agent streams its reply: a soft on/off at a typing cadence. */
|
|
373
|
+
blink: { repeat: Infinity, repeatType: 'mirror', ease: ease.standard, duration: 0.53 },
|
|
374
|
+
/**
|
|
375
|
+
* AI shimmer text (shadcn AI Shimmer / AI Elements): animate `backgroundPosition` from
|
|
376
|
+
* `100% center` → `0% center` forever so a clipped gradient sweeps across the glyphs.
|
|
377
|
+
* Duration matches the AI Elements default (2s).
|
|
378
|
+
*/
|
|
379
|
+
shimmer: { repeat: Infinity, repeatType: 'loop', ease: 'linear', duration: 2 },
|
|
380
|
+
/** Vibe skeleton wave: a highlight band crosses the placeholder, then rests before the next. */
|
|
381
|
+
sweep: {
|
|
382
|
+
repeat: Infinity,
|
|
383
|
+
repeatType: 'loop',
|
|
384
|
+
ease: ease.standard,
|
|
385
|
+
duration: 1.6,
|
|
386
|
+
repeatDelay: 0.4,
|
|
387
|
+
},
|
|
388
|
+
};
|
|
389
|
+
/**
|
|
390
|
+
* `RunningIndicator`: the 8px core scales 0.88 to 1 and the 2px halo fades out and in, on the same
|
|
391
|
+
* 2s loop as the iOS dot (`RunningIndicator.swift`), cosine-like so both ends rest. Nothing fades
|
|
392
|
+
* the core. The core animates a whole `transform` string, not `scale`, so motion hands the loop to
|
|
393
|
+
* the Web Animations API and it keeps running on the compositor while a phone webview scrolls or
|
|
394
|
+
* its main thread is busy. Reduced motion stops the whole loop (the dot renders static, no halo),
|
|
395
|
+
* unlike `loops.pulse`, which carries on.
|
|
396
|
+
*/
|
|
397
|
+
const runningIndicatorEase = [0.37, 0, 0.63, 1];
|
|
398
|
+
export const runningIndicator = {
|
|
399
|
+
core: { transform: ['scale(1)', 'scale(0.88)', 'scale(1)'] },
|
|
400
|
+
halo: { opacity: [1, 0, 1] },
|
|
401
|
+
transition: { repeat: Infinity, repeatType: 'loop', ease: runningIndicatorEase, duration: 2 },
|
|
402
|
+
};
|
|
403
|
+
/**
|
|
404
|
+
* Stable `animate` / `transition` bags for infinite loops. Pass these by reference — never build
|
|
405
|
+
* a fresh `{ opacity: [1, 0.55, 1], … }` object (or a fresh keyframe array) inside render.
|
|
406
|
+
* Motion treats a new `animate` identity as a new animation and restarts the loop; chat Trace /
|
|
407
|
+
* ShimmerText re-render on every agent chunk, so inline targets look "stuck" until a full refresh.
|
|
408
|
+
* Mutable arrays (not `as const`) so they stay assignable to motion's keyframe types.
|
|
409
|
+
*/
|
|
410
|
+
const spinnerOpacityPulse = [1, 0.55, 1];
|
|
411
|
+
export const loopAnimate = {
|
|
412
|
+
/** Spinner / Trace running icon: opacity pulse + WAAPI spin. */
|
|
413
|
+
spinner: {
|
|
414
|
+
opacity: spinnerOpacityPulse,
|
|
415
|
+
transform: ['rotate(0deg)', 'rotate(360deg)'],
|
|
416
|
+
},
|
|
417
|
+
/** Same pulse without the turn (reduced motion / `prefers-reduced-motion`). */
|
|
418
|
+
spinnerReduced: {
|
|
419
|
+
opacity: spinnerOpacityPulse,
|
|
420
|
+
},
|
|
421
|
+
/** ShimmerText / Trace running title: gradient sweep across glyphs. */
|
|
422
|
+
shimmer: {
|
|
423
|
+
backgroundPosition: ['100% center', '0% center'],
|
|
424
|
+
},
|
|
425
|
+
};
|
|
426
|
+
export const loopTransition = {
|
|
427
|
+
spinner: { transform: loops.spin, opacity: loops.pulse },
|
|
428
|
+
shimmer: loops.shimmer,
|
|
429
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The only door to `motion` (motion.dev). Nothing else in the repo imports `motion/react`; apps
|
|
3
|
+
* get all of this through `@streamsai/ui`. Tune timing in `config.ts`, not at call sites.
|
|
4
|
+
*/
|
|
5
|
+
export { LayoutGroup, animate, motion, useMotionValue, useReducedMotion, useTransform, type HTMLMotionProps, type MotionValue, type Transition, type Variants, } from 'motion/react';
|
|
6
|
+
export { duration, ease, spring, distance, scale, stagger, transitions, press, presets, loops, runningIndicator, loopAnimate, loopTransition, popup, sheet, slide, resize, reflow, glide, edgeReveal, stepSwap, scrollReveal, staggerDelay, } from './config.js';
|
|
7
|
+
export { MotionProvider, disableMotion, enableMotion, enablePresenceExits, disablePresenceExits, AnimatePresence, PageTransition, Collapse, AutoHeight, usePrefersReducedMotion, Swap, swapVariants, StaggerItem, type PageTransitionProps, type CollapseProps, type AutoHeightProps, type SwapProps, type SwapPresetName, type StaggerItemProps, } from './components.js';
|
|
8
|
+
export { useAnyModalOpen } from './modal-presence.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The only door to `motion` (motion.dev). Nothing else in the repo imports `motion/react`; apps
|
|
3
|
+
* get all of this through `@streamsai/ui`. Tune timing in `config.ts`, not at call sites.
|
|
4
|
+
*/
|
|
5
|
+
export { LayoutGroup, animate, motion, useMotionValue, useReducedMotion, useTransform, } from 'motion/react';
|
|
6
|
+
export { duration, ease, spring, distance, scale, stagger, transitions, press, presets, loops, runningIndicator, loopAnimate, loopTransition, popup, sheet, slide, resize, reflow, glide, edgeReveal, stepSwap, scrollReveal, staggerDelay, } from './config.js';
|
|
7
|
+
export { MotionProvider, disableMotion, enableMotion, enablePresenceExits, disablePresenceExits, AnimatePresence, PageTransition, Collapse, AutoHeight, usePrefersReducedMotion, Swap, swapVariants, StaggerItem, } from './components.js';
|
|
8
|
+
export { useAnyModalOpen } from './modal-presence.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Called while a modal popup (dialog, alert dialog, sheet) is mounted; returns its cleanup. */
|
|
2
|
+
declare function registerModal(): () => void;
|
|
3
|
+
/** True while any modal popup is mounted, including through its exit animation. */
|
|
4
|
+
declare function useAnyModalOpen(): boolean;
|
|
5
|
+
export { registerModal, useAnyModalOpen };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useSyncExternalStore } from 'react';
|
|
2
|
+
let openModals = 0;
|
|
3
|
+
const listeners = new Set();
|
|
4
|
+
function emit() {
|
|
5
|
+
for (const listener of listeners)
|
|
6
|
+
listener();
|
|
7
|
+
}
|
|
8
|
+
function subscribe(listener) {
|
|
9
|
+
listeners.add(listener);
|
|
10
|
+
return () => {
|
|
11
|
+
listeners.delete(listener);
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/** Called while a modal popup (dialog, alert dialog, sheet) is mounted; returns its cleanup. */
|
|
15
|
+
function registerModal() {
|
|
16
|
+
openModals += 1;
|
|
17
|
+
emit();
|
|
18
|
+
return () => {
|
|
19
|
+
openModals -= 1;
|
|
20
|
+
emit();
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** True while any modal popup is mounted, including through its exit animation. */
|
|
24
|
+
function useAnyModalOpen() {
|
|
25
|
+
return useSyncExternalStore(subscribe, () => openModals > 0, () => false);
|
|
26
|
+
}
|
|
27
|
+
export { registerModal, useAnyModalOpen };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { HTMLMotionProps, TargetAndTransition } from 'motion/react';
|
|
2
|
+
import type { RefObject } from 'react';
|
|
3
|
+
import { popup } from './config.js';
|
|
4
|
+
/**
|
|
5
|
+
* Base UI popups (dialog, sheet, popover, menu, select, tooltip, hover card) animate with `motion`,
|
|
6
|
+
* not CSS. Base UI owns open state, focus and positioning; `motion` owns the animation. The
|
|
7
|
+
* handshake: the Root wrapper asks Base UI to keep a closing popup mounted
|
|
8
|
+
* (`usePopupExit`), the Popup renders through `MotionSurface`, and when its exit animation
|
|
9
|
+
* settles it calls back to unmount.
|
|
10
|
+
*/
|
|
11
|
+
interface MotionPreset {
|
|
12
|
+
initial: TargetAndTransition;
|
|
13
|
+
animate: TargetAndTransition;
|
|
14
|
+
exit: TargetAndTransition;
|
|
15
|
+
}
|
|
16
|
+
/** The nearest popup Root's unmount, for its Popup to call once the exit animation has played. */
|
|
17
|
+
declare const PopupExitContext: import("react").Context<(() => void) | null>;
|
|
18
|
+
interface Unmountable {
|
|
19
|
+
unmount: () => void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* For a Base UI Root wrapper: keeps the popup mounted when it closes and returns the `unmount`
|
|
23
|
+
* its `MotionSurface` calls after the exit. Pass the result's `actionsRef` and `onOpenChange` to
|
|
24
|
+
* the Root and provide `unmount` through `PopupExitContext`.
|
|
25
|
+
*/
|
|
26
|
+
declare function usePopupExit<Actions extends Unmountable, Details>(actionsRef: RefObject<Actions | null> | undefined, onOpenChange: ((open: boolean, details: Details) => void) | undefined): {
|
|
27
|
+
actionsRef: RefObject<Actions | null>;
|
|
28
|
+
onOpenChange: (open: boolean, details: Details) => void;
|
|
29
|
+
unmount: () => void | undefined;
|
|
30
|
+
};
|
|
31
|
+
declare function usePopupExitComplete(): (() => void) | null;
|
|
32
|
+
interface MotionSurfaceProps extends Omit<HTMLMotionProps<'div'>, 'initial' | 'animate' | 'exit'> {
|
|
33
|
+
open: boolean;
|
|
34
|
+
preset: MotionPreset;
|
|
35
|
+
/** Show and hide without animating (a tooltip moving between adjacent triggers). */
|
|
36
|
+
instant?: boolean;
|
|
37
|
+
/** Called once when the closing animation settles. */
|
|
38
|
+
onExitComplete?: (() => void) | null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The element a Base UI popup part renders as: `render={(props, state) => <MotionSurface
|
|
42
|
+
* {...props} open={state.open} … />}`. Animates to `preset.animate` while open and to
|
|
43
|
+
* `preset.exit` while closing, then reports the exit so Base UI can unmount.
|
|
44
|
+
*/
|
|
45
|
+
declare function MotionSurface({ open, preset, instant, onExitComplete, onAnimationComplete, ...props }: MotionSurfaceProps): import("react").JSX.Element;
|
|
46
|
+
type PopupSide = Parameters<typeof popup>[0];
|
|
47
|
+
/**
|
|
48
|
+
* `scale` is the default scale-and-fade; `drift` and `drift-tip` fade and drift only (a dropdown
|
|
49
|
+
* list, and a tip that arrives faster) and `fade` only fades, for a popup that belongs to a dense form.
|
|
50
|
+
*/
|
|
51
|
+
export type PopupVariant = 'scale' | 'drift' | 'drift-tip' | 'fade';
|
|
52
|
+
interface PopupPartState {
|
|
53
|
+
open: boolean;
|
|
54
|
+
side?: PopupSide;
|
|
55
|
+
/** Tooltips only: set when Base UI wants the change shown without a transition. */
|
|
56
|
+
instant?: string | undefined;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* `render` for a positioned popup (popover, menu, select, tooltip, hover card): scales and fades
|
|
60
|
+
* out of the trigger on whichever side Base UI placed it, and hands unmounting back on close.
|
|
61
|
+
*/
|
|
62
|
+
declare function usePopupRender(variant?: PopupVariant): (props: object, state: PopupPartState) => import("react").JSX.Element;
|
|
63
|
+
/** `render` for a modal Popup (dialog, alert dialog, sheet) with the given preset. */
|
|
64
|
+
declare function useModalRender(preset: MotionPreset): (props: object, state: PopupPartState) => import("react").JSX.Element;
|
|
65
|
+
/** `render` for a Backdrop: fades with its popup, which owns the unmount. */
|
|
66
|
+
declare function renderBackdrop(props: object, state: PopupPartState): import("react").JSX.Element;
|
|
67
|
+
interface MotionCollapsePanelProps extends Omit<HTMLMotionProps<'div'>, 'initial' | 'animate'> {
|
|
68
|
+
open: boolean;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Collapsible content. Base UI's Panel is kept mounted so the close can animate; this element
|
|
72
|
+
* then renders nothing once the collapse settles, so closed content is not in the DOM. Content
|
|
73
|
+
* that starts open does not grow in on first render; every later open does.
|
|
74
|
+
*/
|
|
75
|
+
declare function MotionCollapsePanel({ open, hidden: _hidden, style, ...props }: MotionCollapsePanelProps): import("react").JSX.Element | null;
|
|
76
|
+
export { MotionSurface, MotionCollapsePanel, PopupExitContext, usePopupExit, usePopupExitComplete, usePopupRender, useModalRender, renderBackdrop, };
|
|
77
|
+
export type { MotionPreset, MotionSurfaceProps, MotionCollapsePanelProps };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { motion } from 'motion/react';
|
|
3
|
+
import { createContext, use, useCallback, useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { useTextDirection } from '../direction/index.js';
|
|
5
|
+
import { drift, duration, popup, presets } from './config.js';
|
|
6
|
+
import { registerModal } from './modal-presence.js';
|
|
7
|
+
/** How long a closing popup may take before it is unmounted regardless (a missed animation end). */
|
|
8
|
+
const EXIT_FALLBACK_MS = Math.round((duration.slow + duration.base) * 1000);
|
|
9
|
+
/** The nearest popup Root's unmount, for its Popup to call once the exit animation has played. */
|
|
10
|
+
const PopupExitContext = createContext(null);
|
|
11
|
+
/**
|
|
12
|
+
* For a Base UI Root wrapper: keeps the popup mounted when it closes and returns the `unmount`
|
|
13
|
+
* its `MotionSurface` calls after the exit. Pass the result's `actionsRef` and `onOpenChange` to
|
|
14
|
+
* the Root and provide `unmount` through `PopupExitContext`.
|
|
15
|
+
*/
|
|
16
|
+
function usePopupExit(actionsRef, onOpenChange) {
|
|
17
|
+
const ownRef = useRef(null);
|
|
18
|
+
const ref = actionsRef ?? ownRef;
|
|
19
|
+
const handleOpenChange = useCallback((open, details) => {
|
|
20
|
+
onOpenChange?.(open, details);
|
|
21
|
+
// Select has no such method: passing `actionsRef` alone already hands it unmounting.
|
|
22
|
+
if (!open)
|
|
23
|
+
details.preventUnmountOnClose?.();
|
|
24
|
+
}, [onOpenChange]);
|
|
25
|
+
const unmount = useCallback(() => ref.current?.unmount(), [ref]);
|
|
26
|
+
return { actionsRef: ref, onOpenChange: handleOpenChange, unmount };
|
|
27
|
+
}
|
|
28
|
+
function usePopupExitComplete() {
|
|
29
|
+
return use(PopupExitContext);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The element a Base UI popup part renders as: `render={(props, state) => <MotionSurface
|
|
33
|
+
* {...props} open={state.open} … />}`. Animates to `preset.animate` while open and to
|
|
34
|
+
* `preset.exit` while closing, then reports the exit so Base UI can unmount.
|
|
35
|
+
*/
|
|
36
|
+
function MotionSurface({ open, preset, instant = false, onExitComplete, onAnimationComplete, ...props }) {
|
|
37
|
+
const exited = useRef(false);
|
|
38
|
+
const completeExit = useCallback(() => {
|
|
39
|
+
if (exited.current || !onExitComplete)
|
|
40
|
+
return;
|
|
41
|
+
exited.current = true;
|
|
42
|
+
onExitComplete();
|
|
43
|
+
}, [onExitComplete]);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (open) {
|
|
46
|
+
exited.current = false;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const timer = setTimeout(completeExit, EXIT_FALLBACK_MS);
|
|
50
|
+
return () => clearTimeout(timer);
|
|
51
|
+
}, [open, completeExit]);
|
|
52
|
+
const target = open ? preset.animate : preset.exit;
|
|
53
|
+
return (_jsx(motion.div, { initial: instant ? false : preset.initial, animate: instant ? { ...target, transition: { duration: 0 } } : target, onAnimationComplete: (definition) => {
|
|
54
|
+
onAnimationComplete?.(definition);
|
|
55
|
+
if (!open)
|
|
56
|
+
completeExit();
|
|
57
|
+
}, ...props }));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* `render` for a positioned popup (popover, menu, select, tooltip, hover card): scales and fades
|
|
61
|
+
* out of the trigger on whichever side Base UI placed it, and hands unmounting back on close.
|
|
62
|
+
*/
|
|
63
|
+
function usePopupRender(variant = 'scale') {
|
|
64
|
+
const onExitComplete = usePopupExitComplete();
|
|
65
|
+
const dir = useTextDirection();
|
|
66
|
+
return useCallback((props, state) => (_jsx(MotionSurface, { ...props, open: state.open, preset: variant === 'scale'
|
|
67
|
+
? popup(state.side, dir)
|
|
68
|
+
: drift(state.side, variant === 'drift-tip' ? 'tip' : variant === 'fade' ? 'fade' : 'list'), instant: Boolean(state.instant), onExitComplete: onExitComplete })), [onExitComplete, dir, variant]);
|
|
69
|
+
}
|
|
70
|
+
/** A modal popup's surface; it exists only while the popup is mounted, so it is the presence signal. */
|
|
71
|
+
function ModalSurface(props) {
|
|
72
|
+
useEffect(registerModal, []);
|
|
73
|
+
return _jsx(MotionSurface, { ...props });
|
|
74
|
+
}
|
|
75
|
+
/** `render` for a modal Popup (dialog, alert dialog, sheet) with the given preset. */
|
|
76
|
+
function useModalRender(preset) {
|
|
77
|
+
const onExitComplete = usePopupExitComplete();
|
|
78
|
+
return useCallback((props, state) => (_jsx(ModalSurface, { ...props, open: state.open, preset: preset, onExitComplete: onExitComplete })), [onExitComplete, preset]);
|
|
79
|
+
}
|
|
80
|
+
/** `render` for a Backdrop: fades with its popup, which owns the unmount. */
|
|
81
|
+
function renderBackdrop(props, state) {
|
|
82
|
+
return (_jsx(MotionSurface, { ...props, open: state.open, preset: presets.fade }));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Collapsible content. Base UI's Panel is kept mounted so the close can animate; this element
|
|
86
|
+
* then renders nothing once the collapse settles, so closed content is not in the DOM. Content
|
|
87
|
+
* that starts open does not grow in on first render; every later open does.
|
|
88
|
+
*/
|
|
89
|
+
function MotionCollapsePanel({ open, hidden: _hidden, style, ...props }) {
|
|
90
|
+
const [closed, setClosed] = useState(!open);
|
|
91
|
+
const [growOnMount, setGrowOnMount] = useState(!open);
|
|
92
|
+
if (open && closed)
|
|
93
|
+
setClosed(false);
|
|
94
|
+
if (closed)
|
|
95
|
+
return null;
|
|
96
|
+
return (_jsx(motion.div, { initial: growOnMount ? presets.collapse.initial : false, animate: open ? presets.collapse.animate : presets.collapse.exit, onAnimationComplete: () => {
|
|
97
|
+
if (open)
|
|
98
|
+
return;
|
|
99
|
+
setClosed(true);
|
|
100
|
+
setGrowOnMount(true);
|
|
101
|
+
}, inert: !open, style: { overflow: 'hidden', ...style }, ...props }));
|
|
102
|
+
}
|
|
103
|
+
export { MotionSurface, MotionCollapsePanel, PopupExitContext, usePopupExit, usePopupExitComplete, usePopupRender, useModalRender, renderBackdrop, };
|