@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.
Files changed (206) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +57 -2
  3. package/dist/components/ui/agent-avatar.d.ts +50 -0
  4. package/dist/components/ui/agent-avatar.js +38 -0
  5. package/dist/components/ui/alert-dialog.d.ts +12 -0
  6. package/dist/components/ui/alert-dialog.js +37 -0
  7. package/dist/components/ui/alert.d.ts +10 -0
  8. package/dist/components/ui/alert.js +28 -0
  9. package/dist/components/ui/assets/loader.json.js +1 -0
  10. package/dist/components/ui/avatar-frame.d.ts +11 -0
  11. package/dist/components/ui/avatar-frame.js +20 -0
  12. package/dist/components/ui/avatar.d.ts +23 -0
  13. package/dist/components/ui/avatar.js +54 -0
  14. package/dist/components/ui/badge.d.ts +7 -0
  15. package/dist/components/ui/badge.js +36 -0
  16. package/dist/components/ui/bar-list.d.ts +40 -0
  17. package/dist/components/ui/bar-list.js +37 -0
  18. package/dist/components/ui/breadcrumb.d.ts +8 -0
  19. package/dist/components/ui/breadcrumb.js +21 -0
  20. package/dist/components/ui/button-group.d.ts +10 -0
  21. package/dist/components/ui/button-group.js +41 -0
  22. package/dist/components/ui/button.d.ts +9 -0
  23. package/dist/components/ui/button.js +39 -0
  24. package/dist/components/ui/card.d.ts +9 -0
  25. package/dist/components/ui/card.js +25 -0
  26. package/dist/components/ui/checkbox.d.ts +3 -0
  27. package/dist/components/ui/checkbox.js +16 -0
  28. package/dist/components/ui/chip.d.ts +13 -0
  29. package/dist/components/ui/chip.js +22 -0
  30. package/dist/components/ui/clickable.d.ts +27 -0
  31. package/dist/components/ui/clickable.js +54 -0
  32. package/dist/components/ui/code-block.d.ts +68 -0
  33. package/dist/components/ui/code-block.js +112 -0
  34. package/dist/components/ui/code-view.d.ts +21 -0
  35. package/dist/components/ui/code-view.js +20 -0
  36. package/dist/components/ui/collapsible.d.ts +6 -0
  37. package/dist/components/ui/collapsible.js +18 -0
  38. package/dist/components/ui/combobox.d.ts +14 -0
  39. package/dist/components/ui/combobox.js +37 -0
  40. package/dist/components/ui/command.d.ts +15 -0
  41. package/dist/components/ui/command.js +34 -0
  42. package/dist/components/ui/context-menu.d.ts +21 -0
  43. package/dist/components/ui/context-menu.js +60 -0
  44. package/dist/components/ui/counter.d.ts +11 -0
  45. package/dist/components/ui/counter.js +20 -0
  46. package/dist/components/ui/dialog.d.ts +17 -0
  47. package/dist/components/ui/dialog.js +41 -0
  48. package/dist/components/ui/disabled-reason.d.ts +22 -0
  49. package/dist/components/ui/disabled-reason.js +16 -0
  50. package/dist/components/ui/dropdown-menu.d.ts +33 -0
  51. package/dist/components/ui/dropdown-menu.js +58 -0
  52. package/dist/components/ui/editable-text.d.ts +95 -0
  53. package/dist/components/ui/editable-text.js +191 -0
  54. package/dist/components/ui/heading.d.ts +24 -0
  55. package/dist/components/ui/heading.js +92 -0
  56. package/dist/components/ui/hover-card.d.ts +9 -0
  57. package/dist/components/ui/hover-card.js +16 -0
  58. package/dist/components/ui/icon-button.d.ts +23 -0
  59. package/dist/components/ui/icon-button.js +32 -0
  60. package/dist/components/ui/input.d.ts +3 -0
  61. package/dist/components/ui/input.js +8 -0
  62. package/dist/components/ui/kbd.d.ts +12 -0
  63. package/dist/components/ui/kbd.js +12 -0
  64. package/dist/components/ui/label.d.ts +3 -0
  65. package/dist/components/ui/label.js +7 -0
  66. package/dist/components/ui/loader.d.ts +19 -0
  67. package/dist/components/ui/loader.js +157 -0
  68. package/dist/components/ui/message-toolbar.d.ts +20 -0
  69. package/dist/components/ui/message-toolbar.js +17 -0
  70. package/dist/components/ui/modal.d.ts +30 -0
  71. package/dist/components/ui/modal.js +41 -0
  72. package/dist/components/ui/nav-item.d.ts +25 -0
  73. package/dist/components/ui/nav-item.js +66 -0
  74. package/dist/components/ui/panel-section.d.ts +22 -0
  75. package/dist/components/ui/panel-section.js +26 -0
  76. package/dist/components/ui/popover.d.ts +14 -0
  77. package/dist/components/ui/popover.js +30 -0
  78. package/dist/components/ui/progress.d.ts +5 -0
  79. package/dist/components/ui/progress.js +16 -0
  80. package/dist/components/ui/provider-badge.d.ts +22 -0
  81. package/dist/components/ui/provider-badge.js +38 -0
  82. package/dist/components/ui/radio-group.d.ts +5 -0
  83. package/dist/components/ui/radio-group.js +21 -0
  84. package/dist/components/ui/reply-summary.d.ts +16 -0
  85. package/dist/components/ui/reply-summary.js +8 -0
  86. package/dist/components/ui/resizable.d.ts +8 -0
  87. package/dist/components/ui/resizable.js +15 -0
  88. package/dist/components/ui/rich-text-editor.d.ts +447 -0
  89. package/dist/components/ui/rich-text-editor.js +2419 -0
  90. package/dist/components/ui/running-indicator.d.ts +17 -0
  91. package/dist/components/ui/running-indicator.js +34 -0
  92. package/dist/components/ui/sandboxed-frame.d.ts +29 -0
  93. package/dist/components/ui/sandboxed-frame.js +77 -0
  94. package/dist/components/ui/scroll-area.d.ts +14 -0
  95. package/dist/components/ui/scroll-area.js +17 -0
  96. package/dist/components/ui/section-header.d.ts +12 -0
  97. package/dist/components/ui/section-header.js +12 -0
  98. package/dist/components/ui/select.d.ts +15 -0
  99. package/dist/components/ui/select.js +40 -0
  100. package/dist/components/ui/separator.d.ts +3 -0
  101. package/dist/components/ui/separator.js +13 -0
  102. package/dist/components/ui/sheet.d.ts +23 -0
  103. package/dist/components/ui/sheet.js +48 -0
  104. package/dist/components/ui/shimmer-text.d.ts +26 -0
  105. package/dist/components/ui/shimmer-text.js +32 -0
  106. package/dist/components/ui/sidebar.d.ts +63 -0
  107. package/dist/components/ui/sidebar.js +283 -0
  108. package/dist/components/ui/skeleton.d.ts +14 -0
  109. package/dist/components/ui/skeleton.js +12 -0
  110. package/dist/components/ui/sonner.d.ts +4 -0
  111. package/dist/components/ui/sonner.js +14 -0
  112. package/dist/components/ui/spark-bars.d.ts +29 -0
  113. package/dist/components/ui/spark-bars.js +24 -0
  114. package/dist/components/ui/spinner.d.ts +16 -0
  115. package/dist/components/ui/spinner.js +26 -0
  116. package/dist/components/ui/stacked-bar.d.ts +37 -0
  117. package/dist/components/ui/stacked-bar.js +56 -0
  118. package/dist/components/ui/status-dot.d.ts +16 -0
  119. package/dist/components/ui/status-dot.js +32 -0
  120. package/dist/components/ui/switch.d.ts +5 -0
  121. package/dist/components/ui/switch.js +13 -0
  122. package/dist/components/ui/tabs.d.ts +14 -0
  123. package/dist/components/ui/tabs.js +55 -0
  124. package/dist/components/ui/text.d.ts +27 -0
  125. package/dist/components/ui/text.js +100 -0
  126. package/dist/components/ui/textarea.d.ts +3 -0
  127. package/dist/components/ui/textarea.js +7 -0
  128. package/dist/components/ui/theme-toggle.d.ts +21 -0
  129. package/dist/components/ui/theme-toggle.js +17 -0
  130. package/dist/components/ui/toggle-group.d.ts +6 -0
  131. package/dist/components/ui/toggle-group.js +12 -0
  132. package/dist/components/ui/tooltip.d.ts +11 -0
  133. package/dist/components/ui/tooltip.js +21 -0
  134. package/dist/components/ui/trace.d.ts +24 -0
  135. package/dist/components/ui/trace.js +57 -0
  136. package/dist/components/ui/trend-line.d.ts +93 -0
  137. package/dist/components/ui/trend-line.js +215 -0
  138. package/dist/components/ui/truncate.d.ts +30 -0
  139. package/dist/components/ui/truncate.js +54 -0
  140. package/dist/components/ui/viewer-shell.d.ts +55 -0
  141. package/dist/components/ui/viewer-shell.js +15 -0
  142. package/dist/components/ui/virtual-list.d.ts +91 -0
  143. package/dist/components/ui/virtual-list.js +406 -0
  144. package/dist/components/ui/visually-hidden.d.ts +15 -0
  145. package/dist/components/ui/visually-hidden.js +19 -0
  146. package/dist/direction/index.d.ts +19 -0
  147. package/dist/direction/index.js +32 -0
  148. package/dist/hooks/use-code-highlighting.d.ts +22 -0
  149. package/dist/hooks/use-code-highlighting.js +47 -0
  150. package/dist/hooks/use-is-truncated.d.ts +7 -0
  151. package/dist/hooks/use-is-truncated.js +23 -0
  152. package/dist/hooks/use-mobile.d.ts +2 -0
  153. package/dist/hooks/use-mobile.js +15 -0
  154. package/dist/icons/custom.d.ts +29 -0
  155. package/dist/icons/custom.js +48 -0
  156. package/dist/icons/index.d.ts +49 -0
  157. package/dist/icons/index.js +50 -0
  158. package/dist/icons/provider-logos.d.ts +17 -0
  159. package/dist/icons/provider-logos.js +28 -0
  160. package/dist/icons/providers.d.ts +9 -0
  161. package/dist/icons/providers.js +51 -0
  162. package/dist/index.d.ts +102 -0
  163. package/dist/index.js +124 -0
  164. package/dist/lib/code-highlighter.d.ts +9 -0
  165. package/dist/lib/code-highlighter.js +65 -0
  166. package/dist/lib/code-language.d.ts +13 -0
  167. package/dist/lib/code-language.js +103 -0
  168. package/dist/lib/code-tokens.d.ts +9 -0
  169. package/dist/lib/code-tokens.js +27 -0
  170. package/dist/lib/copy-text.d.ts +6 -0
  171. package/dist/lib/copy-text.js +46 -0
  172. package/dist/lib/home-tile.d.ts +12 -0
  173. package/dist/lib/home-tile.js +45 -0
  174. package/dist/lib/reveal-mask.d.ts +9 -0
  175. package/dist/lib/reveal-mask.js +16 -0
  176. package/dist/lib/text-direction.d.ts +28 -0
  177. package/dist/lib/text-direction.js +119 -0
  178. package/dist/lib/utils.d.ts +11 -0
  179. package/dist/lib/utils.js +30 -0
  180. package/dist/motion/components.d.ts +184 -0
  181. package/dist/motion/components.js +211 -0
  182. package/dist/motion/config.d.ts +849 -0
  183. package/dist/motion/config.js +429 -0
  184. package/dist/motion/index.d.ts +8 -0
  185. package/dist/motion/index.js +8 -0
  186. package/dist/motion/modal-presence.d.ts +5 -0
  187. package/dist/motion/modal-presence.js +27 -0
  188. package/dist/motion/popup.d.ts +77 -0
  189. package/dist/motion/popup.js +103 -0
  190. package/dist/platform/index.d.ts +29 -0
  191. package/dist/platform/index.js +63 -0
  192. package/dist/styles/base.css +319 -0
  193. package/dist/styles/fonts/Figtree-Variable.woff2 +0 -0
  194. package/dist/styles/fonts/OFL.txt +93 -0
  195. package/dist/styles/fonts/Poppins-Medium.woff2 +0 -0
  196. package/dist/styles/fonts/Poppins-Regular.woff2 +0 -0
  197. package/dist/styles/fonts/Poppins-SemiBold.woff2 +0 -0
  198. package/dist/styles/fonts.css +28 -0
  199. package/dist/styles/globals.css +8 -0
  200. package/dist/styles/mcp-host.css +76 -0
  201. package/dist/styles/tokens.css +387 -0
  202. package/dist/styles/tokens.d.mts +57 -0
  203. package/dist/styles/tokens.mjs +59 -0
  204. package/dist/theme/index.d.ts +28 -0
  205. package/dist/theme/index.js +65 -0
  206. package/package.json +136 -3
@@ -0,0 +1,215 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useId, useState } from 'react';
3
+ import { AnimatePresence, motion } from '../../motion/index.js';
4
+ import { presets, transitions } from '../../motion/config.js';
5
+ import { cn } from '../../lib/utils.js';
6
+ import { Skeleton } from './skeleton.js';
7
+ // Shared with `TrendLineSkeleton`: change a size here and the skeleton follows.
8
+ const SINGLE_ROOT_CLASS = 'relative flex h-40 w-full gap-2 text-xs text-muted-foreground';
9
+ const MULTI_ROOT_CLASS = 'relative flex w-full flex-col gap-3 text-xs text-muted-foreground';
10
+ const MULTI_PLOT_CLASS = 'relative mb-5 flex h-40 gap-2';
11
+ const X_AXIS_CLASS = 'absolute inset-x-0 -bottom-5 flex justify-between';
12
+ const VIEW_W = 100;
13
+ const VIEW_H = 40;
14
+ const GRIDLINES = 3;
15
+ const gridFractions = Array.from({ length: GRIDLINES }, (_, i) => i / (GRIDLINES - 1));
16
+ const buildPaths = (points, all, max) => {
17
+ // No history: nothing to draw (and no last real coordinate to end the area under), even when
18
+ // a projection is passed.
19
+ if (points.length === 0)
20
+ return { line: '', area: '' };
21
+ const step = all.length > 1 ? VIEW_W / (all.length - 1) : 0;
22
+ const coordsAll = all.map((point, index) => {
23
+ const x = all.length > 1 ? index * step : VIEW_W / 2;
24
+ const y = VIEW_H - (point.value / max) * VIEW_H;
25
+ return { x, y };
26
+ });
27
+ const coordsPoints = coordsAll.slice(0, points.length);
28
+ const projected = all.length > points.length;
29
+ if (coordsPoints.length === 1) {
30
+ const only = coordsPoints[0];
31
+ // With a projection the lone real point sits at its own x (a dot); the dashed line goes on.
32
+ if (projected)
33
+ return {
34
+ line: `M${only.x},${only.y} L${only.x},${only.y}`,
35
+ area: `M${only.x},${only.y} L${only.x},${VIEW_H} Z`,
36
+ };
37
+ return {
38
+ line: `M0,${only.y} L${VIEW_W},${only.y}`,
39
+ area: `M0,${only.y} L${VIEW_W},${only.y} L${VIEW_W},${VIEW_H} L0,${VIEW_H} Z`,
40
+ };
41
+ }
42
+ const line = coordsPoints.map((c, i) => `${i === 0 ? 'M' : 'L'}${c.x},${c.y}`).join(' ');
43
+ // The area ends under the last real point: no area under the projection.
44
+ const lastX = projected ? coordsPoints[coordsPoints.length - 1].x : VIEW_W;
45
+ const area = `${line} L${lastX},${VIEW_H} L0,${VIEW_H} Z`;
46
+ return { line, area };
47
+ };
48
+ const buildProjectionPath = (points, projection, all, max) => {
49
+ const step = all.length > 1 ? VIEW_W / (all.length - 1) : 0;
50
+ const getCoord = (index, value) => ({
51
+ x: all.length > 1 ? index * step : VIEW_W / 2,
52
+ y: VIEW_H - (value / max) * VIEW_H,
53
+ });
54
+ // Start from the last real point
55
+ const lastReal = points[points.length - 1];
56
+ if (!lastReal)
57
+ return '';
58
+ const startCoord = getCoord(points.length - 1, lastReal.value);
59
+ const projCoords = projection.map((pt, i) => getCoord(points.length + i, pt.value));
60
+ return [startCoord, ...projCoords].map((c, i) => `${i === 0 ? 'M' : 'L'}${c.x},${c.y}`).join(' ');
61
+ };
62
+ /**
63
+ * A line with a soft area under it, three gridlines and one tooltip. Hand-rolled SVG on a
64
+ * fixed viewBox stretched to the box (`preserveAspectRatio="none"`); `vectorEffect` keeps the
65
+ * stroke one pixel wide whatever the aspect. The SVG is decoration: an invisible button per
66
+ * point carries the data for pointers, keyboards and screen readers, and a visually hidden
67
+ * table repeats it for anyone who reads instead of hovers.
68
+ *
69
+ * On mount the line and its area draw in from the left (a clip that grows to full width, which
70
+ * reads the same as a stroke draw because x only ever increases, and unlike `pathLength` it is
71
+ * not thrown off by `non-scaling-stroke`). The hover guide and tooltip fade in and out.
72
+ *
73
+ * Pass `projection` to render a dashed line continuing from the last real point through
74
+ * projected future values; hover buttons and the hidden table cover all points.
75
+ */
76
+ function SingleSeriesTrendLine({ points, formatValue = String, formatValueDetail, projection, projectedLabel = 'projected', className, 'aria-label': ariaLabel, ...props }) {
77
+ const [active, setActive] = useState(null);
78
+ const tableId = useId();
79
+ const clipId = useId();
80
+ const all = projection ? [...points, ...projection] : points;
81
+ const max = Math.max(1, ...all.map((point) => point.value));
82
+ const { line, area } = buildPaths(points, all, max);
83
+ const projectionPath = projection && projection.length > 0 && points.length > 0
84
+ ? buildProjectionPath(points, projection, all, max)
85
+ : null;
86
+ const activePoint = active === null ? null : (all[active] ?? null);
87
+ const isActiveProjected = active !== null && active >= points.length;
88
+ const activeDetail = activePoint && !isActiveProjected ? formatValueDetail?.(activePoint.value) : undefined;
89
+ const activeLeft = active === null || all.length < 2 ? 50 : (active / (all.length - 1)) * 100;
90
+ return (_jsxs("figure", { "data-slot": "trend-line", "aria-label": ariaLabel, className: cn(SINGLE_ROOT_CLASS, className), ...props, children: [_jsx("div", { "aria-hidden": "true", className: "flex shrink-0 flex-col justify-between py-px text-right tabular-nums", children: gridFractions.map((fraction) => (_jsx("span", { children: formatValue(Math.round(max * (1 - fraction))) }, fraction))) }), _jsxs("div", { className: "relative min-w-0 flex-1", onMouseLeave: () => setActive(null), children: [_jsxs("svg", { "aria-hidden": "true", viewBox: `0 0 ${VIEW_W} ${VIEW_H}`, preserveAspectRatio: "none", className: "absolute inset-0 size-full overflow-visible", children: [gridFractions.map((fraction) => (_jsx("line", { x1: "0", x2: VIEW_W, y1: fraction * VIEW_H, y2: fraction * VIEW_H, vectorEffect: "non-scaling-stroke", className: "stroke-border", strokeWidth: "1", strokeDasharray: fraction === 1 ? undefined : '2 3' }, fraction))), points.length > 0 && (_jsxs(_Fragment, { children: [_jsx("defs", { children: _jsx("clipPath", { id: clipId, children: _jsx(motion.rect, { x: -1, y: -VIEW_H, height: VIEW_H * 3, initial: { width: 0 }, animate: { width: VIEW_W + 2 }, transition: transitions.slow }) }) }), _jsxs("g", { clipPath: `url(#${clipId})`, children: [_jsx("path", { d: area, className: "fill-chart-1/15" }), _jsx("path", { d: line, fill: "none", vectorEffect: "non-scaling-stroke", strokeWidth: "1.5", strokeLinejoin: "round", strokeLinecap: "round", className: "stroke-chart-1" }), projectionPath && (_jsx("path", { d: projectionPath, fill: "none", vectorEffect: "non-scaling-stroke", strokeWidth: "1.5", strokeLinejoin: "round", strokeLinecap: "round", strokeDasharray: "2 2", className: "stroke-chart-1/50", "data-slot": "trend-line-projection" }))] })] })), _jsx(AnimatePresence, { children: active !== null && (_jsx(motion.line, { ...presets.fade, x1: (activeLeft / 100) * VIEW_W, x2: (activeLeft / 100) * VIEW_W, y1: "0", y2: VIEW_H, vectorEffect: "non-scaling-stroke", strokeWidth: "1", className: "stroke-foreground/40" }, "guide")) })] }), _jsx("div", { className: "absolute inset-0 flex", "aria-describedby": tableId, children: all.map((point, index) => {
91
+ const isProjected = index >= points.length;
92
+ const label = isProjected
93
+ ? `${point.label}: ${formatValue(point.value)} (${projectedLabel})`
94
+ : `${point.label}: ${formatValue(point.value)}`;
95
+ return (_jsx("button", { type: "button", "aria-label": label, "data-active": active === index || undefined, className: "min-w-0 flex-1 cursor-crosshair rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-ring/50", onMouseEnter: () => setActive(index), onFocus: () => setActive(index), onBlur: () => setActive(null) }, `${point.label}-${index}`));
96
+ }) }), _jsx(AnimatePresence, { children: activePoint && (_jsxs(motion.div, { ...presets.fade, role: "presentation", "aria-hidden": "true", "data-slot": "trend-line-tooltip", className: "pointer-events-none absolute -top-1 z-10 -translate-x-1/2 -translate-y-full rounded-md bg-foreground px-2 py-1 text-xs whitespace-nowrap text-background shadow-md", style: { left: `${activeLeft}%` }, children: [_jsx("span", { className: "font-semibold tabular-nums", children: formatValue(activePoint.value) }), _jsx("span", { className: "ml-1.5 opacity-70", children: activePoint.label }), isActiveProjected && _jsxs("span", { className: "ml-1 opacity-70", children: ["(", projectedLabel, ")"] }), activeDetail ? (_jsx("div", { className: "mt-0.5 opacity-70 tabular-nums", children: activeDetail })) : null] }, "tooltip")) }), _jsxs("div", { className: X_AXIS_CLASS, "aria-hidden": "true", children: [_jsx("span", { children: points[0]?.label }), all.length > 1 && _jsx("span", { children: all[all.length - 1]?.label })] })] }), _jsxs("table", { id: tableId, className: "sr-only", children: [_jsx("caption", { children: ariaLabel }), _jsx("tbody", { children: all.map((point, index) => {
97
+ const isProjected = index >= points.length;
98
+ return (_jsxs("tr", { children: [_jsx("th", { scope: "row", children: point.label }), _jsxs("td", { children: [formatValue(point.value), isProjected && ` (${projectedLabel})`] })] }, `${point.label}-${index}`));
99
+ }) })] })] }));
100
+ }
101
+ /**
102
+ * Colour AND shape differ per series, so overlapping lines stay readable without colour: solid
103
+ * with round dots, dashed with diamond dots, dotted with square dots. Colours are chart tokens.
104
+ */
105
+ const MULTI_STYLES = [
106
+ {
107
+ stroke: 'stroke-chart-1',
108
+ fill: 'fill-chart-1',
109
+ dash: undefined,
110
+ dot: 'rounded-full bg-chart-1',
111
+ },
112
+ {
113
+ stroke: 'stroke-chart-3',
114
+ fill: 'fill-chart-3',
115
+ dash: '5 3',
116
+ dot: 'rotate-45 rounded-[1px] bg-chart-3',
117
+ },
118
+ { stroke: 'stroke-chart-2', fill: 'fill-chart-2', dash: '1 3', dot: 'rounded-[1px] bg-chart-2' },
119
+ ];
120
+ /** Dots on every point up to this many points; beyond it only the first, last and active. */
121
+ const MAX_ALL_DOTS = 31;
122
+ /**
123
+ * Zero-based integer axis: at most two intervals, every tick a whole number. The top tick is
124
+ * the smallest multiple of the step that holds `max`, so no line is ever clipped.
125
+ */
126
+ const integerTicks = (max) => {
127
+ const top = Math.max(1, Math.ceil(max));
128
+ const intervals = Math.min(top, GRIDLINES - 1);
129
+ const step = Math.ceil(top / intervals);
130
+ return Array.from({ length: intervals + 1 }, (_, i) => i * step);
131
+ };
132
+ function MultiSeriesTrendLine({ multiSeries, formatValue = String, onPointActivate, className, 'aria-label': ariaLabel, ...props }) {
133
+ const [active, setActive] = useState(null);
134
+ const tableId = useId();
135
+ const clipId = useId();
136
+ const { names, points } = multiSeries;
137
+ const ticks = integerTicks(Math.max(0, ...points.flatMap((point) => point.values)));
138
+ const top = ticks[ticks.length - 1];
139
+ const first = points[0]?.timestamp ?? 0;
140
+ const span = (points[points.length - 1]?.timestamp ?? 0) - first;
141
+ // x is the timestamp's share of the span, so a short last interval stays short. The SVG is not
142
+ // mirrored in RTL, so the overlay below uses physical `left` to stay aligned with it.
143
+ const xs = points.map((point) => (span > 0 ? ((point.timestamp - first) / span) * VIEW_W : 50));
144
+ const yOf = (value) => VIEW_H - (Math.min(value, top) / top) * VIEW_H;
145
+ const paths = names.map((_, series) => xs.length < 2
146
+ ? ''
147
+ : points
148
+ .map((point, i) => `${i === 0 ? 'M' : 'L'}${xs[i]},${yOf(point.values[series] ?? 0)}`)
149
+ .join(' '));
150
+ const showAllDots = points.length <= MAX_ALL_DOTS;
151
+ const dotIndexes = new Set(showAllDots
152
+ ? points.map((_, i) => i)
153
+ : [0, points.length - 1, ...(active === null ? [] : [active])]);
154
+ const activePoint = active === null ? null : (points[active] ?? null);
155
+ const activeLeft = active === null ? 50 : (xs[active] ?? 50);
156
+ const tooltipShift = activeLeft < 20 ? 'translate-x-0' : activeLeft > 80 ? '-translate-x-full' : '-translate-x-1/2';
157
+ // Each target spans from the midpoint before it to the midpoint after it.
158
+ const bounds = xs.map((x, i) => {
159
+ const start = i === 0 ? 0 : (xs[i - 1] + x) / 2;
160
+ const end = i === xs.length - 1 ? VIEW_W : (x + xs[i + 1]) / 2;
161
+ return { start, width: Math.max(0, end - start) };
162
+ });
163
+ const pointLabel = (point) => [
164
+ `${point.label}: ${names.map((name, s) => `${name} ${formatValue(point.values[s] ?? 0)}`).join(', ')}`,
165
+ point.note,
166
+ point.activateLabel,
167
+ ]
168
+ .filter(Boolean)
169
+ .join('. ');
170
+ return (_jsxs("figure", { "data-slot": "trend-line", "data-mode": "multi", "aria-label": ariaLabel, className: cn(MULTI_ROOT_CLASS, className), ...props, children: [_jsx("ul", { "data-slot": "trend-line-legend", className: "flex flex-wrap items-center gap-x-4 gap-y-1", children: names.map((name, series) => {
171
+ const style = MULTI_STYLES[series % MULTI_STYLES.length];
172
+ return (_jsxs("li", { className: "flex items-center gap-1.5", children: [_jsxs("svg", { "aria-hidden": "true", viewBox: "0 0 16 8", className: "h-2 w-4 shrink-0", children: [_jsx("line", { x1: "0", x2: "16", y1: "4", y2: "4", strokeWidth: "1.5", strokeDasharray: style.dash, className: style.stroke }), _jsx("rect", { x: "5.5", y: "1.5", width: "5", height: "5", rx: series % MULTI_STYLES.length === 0 ? 2.5 : 0.5, transform: series % MULTI_STYLES.length === 1 ? 'rotate(45 8 4)' : undefined, className: style.fill })] }), _jsx("span", { children: name })] }, `${name}-${series}`));
173
+ }) }), _jsxs("div", { className: MULTI_PLOT_CLASS, children: [_jsx("div", { "aria-hidden": "true", "data-slot": "trend-line-y-axis", className: "flex shrink-0 flex-col-reverse justify-between py-px text-end tabular-nums", children: ticks.map((tick) => (_jsx("span", { children: formatValue(tick) }, tick))) }), _jsxs("div", { className: "relative min-w-0 flex-1", onMouseLeave: () => setActive(null), children: [_jsxs("svg", { "aria-hidden": "true", "data-slot": "trend-line-plot", viewBox: `0 0 ${VIEW_W} ${VIEW_H}`, preserveAspectRatio: "none", className: "absolute inset-0 size-full overflow-visible", children: [ticks.map((tick) => (_jsx("line", { x1: "0", x2: VIEW_W, y1: yOf(tick), y2: yOf(tick), vectorEffect: "non-scaling-stroke", className: "stroke-border", strokeWidth: "1", strokeDasharray: tick === 0 ? undefined : '2 3' }, tick))), points.length > 0 && (_jsxs(_Fragment, { children: [_jsx("defs", { children: _jsx("clipPath", { id: clipId, children: _jsx(motion.rect, { x: -1, y: -VIEW_H, height: VIEW_H * 3, initial: { width: 0 }, animate: { width: VIEW_W + 2 }, transition: transitions.slow }) }) }), _jsx("g", { clipPath: `url(#${clipId})`, children: paths.map((d, series) => {
174
+ if (!d)
175
+ return null;
176
+ const style = MULTI_STYLES[series % MULTI_STYLES.length];
177
+ return (_jsx("path", { d: d, fill: "none", vectorEffect: "non-scaling-stroke", strokeWidth: "1.5", strokeLinejoin: "round", strokeDasharray: style.dash, className: style.stroke, "data-slot": "trend-line-series", "data-series": series }, series));
178
+ }) })] })), _jsx(AnimatePresence, { children: active !== null && (_jsx(motion.line, { ...presets.fade, x1: activeLeft, x2: activeLeft, y1: "0", y2: VIEW_H, vectorEffect: "non-scaling-stroke", strokeWidth: "1", className: "stroke-foreground/40" }, "guide")) })] }), _jsx("div", { "aria-hidden": "true", className: "pointer-events-none absolute inset-0", children: points.map((point, index) => dotIndexes.has(index)
179
+ ? names.map((_, series) => {
180
+ const style = MULTI_STYLES[series % MULTI_STYLES.length];
181
+ return (_jsx("span", { "data-slot": "trend-line-dot", "data-series": series, className: cn('absolute size-1.5 -translate-x-1/2 -translate-y-1/2 ring-1 ring-background', style.dot, active === index && 'size-2'), style: {
182
+ left: `${xs[index]}%`,
183
+ top: `${(yOf(point.values[series] ?? 0) / VIEW_H) * 100}%`,
184
+ } }, `${index}-${series}`));
185
+ })
186
+ : null) }), _jsx("div", { className: "absolute inset-0", "aria-describedby": tableId, children: points.map((point, index) => {
187
+ const activatable = Boolean(point.activateLabel && onPointActivate);
188
+ return (_jsx("button", { type: "button", "aria-label": pointLabel(point), "data-active": active === index || undefined, "data-activatable": activatable || undefined, className: cn('absolute inset-y-0 min-w-0 rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-ring/50', activatable ? 'cursor-pointer' : 'cursor-crosshair'), style: { left: `${bounds[index]?.start}%`, width: `${bounds[index]?.width}%` }, onMouseEnter: () => setActive(index), onFocus: () => setActive(index), onBlur: () => setActive(null), onClick: () => {
189
+ if (activatable)
190
+ onPointActivate?.(index);
191
+ } }, `${point.timestamp}-${index}`));
192
+ }) }), _jsx(AnimatePresence, { children: activePoint && (_jsxs(motion.div, { ...presets.fade, role: "presentation", "aria-hidden": "true", "data-slot": "trend-line-tooltip", className: cn('pointer-events-none absolute -top-1 z-10 -translate-y-full rounded-md bg-foreground px-2 py-1 text-xs whitespace-nowrap text-background shadow-md', tooltipShift), style: { left: `${activeLeft}%` }, children: [_jsx("div", { className: "font-semibold", children: activePoint.label }), activePoint.note ? _jsx("div", { className: "opacity-70", children: activePoint.note }) : null, names.map((name, series) => (_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("span", { className: "opacity-70", children: name }), _jsx("span", { className: "font-semibold tabular-nums", children: formatValue(activePoint.values[series] ?? 0) })] }, `${name}-${series}`)))] }, "tooltip")) }), _jsxs("div", { className: X_AXIS_CLASS, "aria-hidden": "true", children: [_jsx("span", { children: points[0]?.axisLabel ?? points[0]?.label }), points.length > 1 && (_jsx("span", { children: points[points.length - 1]?.axisLabel ?? points[points.length - 1]?.label }))] })] })] }), _jsxs("table", { id: tableId, className: "sr-only", children: [_jsx("caption", { children: ariaLabel }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("td", {}), names.map((name, series) => (_jsx("th", { scope: "col", children: name }, `${name}-${series}`)))] }) }), _jsx("tbody", { children: points.map((point, index) => (_jsxs("tr", { children: [_jsx("th", { scope: "row", children: point.label }), names.map((name, series) => (_jsx("td", { children: formatValue(point.values[series] ?? 0) }, `${name}-${series}`)))] }, `${point.timestamp}-${index}`))) })] })] }));
193
+ }
194
+ function TrendLine(props) {
195
+ if (props.multiSeries !== undefined)
196
+ return _jsx(MultiSeriesTrendLine, { ...props });
197
+ return _jsx(SingleSeriesTrendLine, { ...props });
198
+ }
199
+ const SKELETON_AREA = 'polygon(0 72%, 14% 48%, 28% 62%, 43% 30%, 58% 52%, 72% 24%, 86% 44%, 100% 18%, 100% 100%, 0 100%)';
200
+ /** Y-axis stubs, dashed grid, an area shape and x-axis stubs: the plot of either trend line. */
201
+ function PlotSkeleton() {
202
+ return (_jsxs(_Fragment, { children: [_jsx("div", { "data-slot": "trend-line-skeleton-y-axis", className: "flex shrink-0 flex-col justify-between py-px", children: [0, 1, 2].map((tick) => (_jsx(Skeleton, { variant: "shimmer", className: "h-3 w-8" }, tick))) }), _jsxs("div", { className: "relative min-w-0 flex-1", children: [['top-0', 'top-1/2', 'bottom-0'].map((position) => (_jsx("div", { className: cn('absolute inset-x-0 border-t border-dashed border-border', position) }, position))), _jsx(Skeleton, { variant: "shimmer", className: "absolute inset-0 rounded-none bg-foreground/10", style: { clipPath: SKELETON_AREA } }), _jsxs("div", { className: X_AXIS_CLASS, children: [_jsx(Skeleton, { variant: "shimmer", className: "h-3 w-10" }), _jsx(Skeleton, { variant: "shimmer", className: "h-3 w-10" })] })] })] }));
203
+ }
204
+ /**
205
+ * The loading shape of `TrendLine`: y-axis stubs, three dashed grid lines, a soft area and
206
+ * x-axis stubs, in the same 160 px plot. With `legendItems` it also draws the legend line of the
207
+ * multi-series variant.
208
+ */
209
+ function TrendLineSkeleton({ legendItems = 0, className, ...props }) {
210
+ if (legendItems > 0) {
211
+ return (_jsxs("div", { "data-slot": "trend-line-skeleton", "aria-hidden": "true", className: cn(MULTI_ROOT_CLASS, className), ...props, children: [_jsx("div", { className: "flex h-4 items-center gap-x-4", children: Array.from({ length: legendItems }, (_, index) => (_jsx(Skeleton, { variant: "shimmer", className: "h-3 w-20" }, index))) }), _jsx("div", { className: MULTI_PLOT_CLASS, children: _jsx(PlotSkeleton, {}) })] }));
212
+ }
213
+ return (_jsx("div", { "data-slot": "trend-line-skeleton", "aria-hidden": "true", className: cn(SINGLE_ROOT_CLASS, className), ...props, children: _jsx(PlotSkeleton, {}) }));
214
+ }
215
+ export { TrendLine, TrendLineSkeleton };
@@ -0,0 +1,30 @@
1
+ import { useRender } from '@base-ui/react/use-render';
2
+ import { type ReactElement, type ReactNode, type RefObject } from 'react';
3
+ /**
4
+ * Shared by `Truncate`, `Text ellipsis` and `Heading ellipsis`: attach your `ref` to the clipped
5
+ * element and wrap it in `TruncationTooltip` with the returned `text` and `truncated`. `tooltip`:
6
+ * `false` opts out, a string overrides the content (needed when the children are not plain text).
7
+ * `enabled: false` leaves `text` undefined, which makes the wrapper a pass-through.
8
+ */
9
+ declare function useTruncationTooltip(ref: RefObject<HTMLElement | null>, enabled: boolean, tooltip: boolean | string | undefined, children: ReactNode): {
10
+ text: string | undefined;
11
+ truncated: boolean;
12
+ };
13
+ /** Tooltip with the full text around `children`; only opens while `truncated`. */
14
+ declare function TruncationTooltip({ text, truncated, children, }: {
15
+ text: string | undefined;
16
+ truncated: boolean;
17
+ children: ReactElement;
18
+ }): import("react").JSX.Element;
19
+ type TruncateProps = useRender.ComponentProps<'span'> & {
20
+ /** `false` turns the tooltip off; a string overrides its content. Default: the children's text. */
21
+ tooltip?: boolean | string;
22
+ };
23
+ /**
24
+ * Typography-free truncation: clips overflowing text with an ellipsis and shows the full text in
25
+ * a tooltip only while it is clipped. Renders a `span`; use `render` to put the same behaviour on
26
+ * another element (`render={<h1 />}`, `render={<button />}`). It adds no display or font classes,
27
+ * so the element must already be a block, flex item or `min-w-0` box that can overflow.
28
+ */
29
+ declare function Truncate({ className, render, tooltip, ref, ...props }: TruncateProps): import("react").JSX.Element;
30
+ export { Truncate, TruncationTooltip, useTruncationTooltip, type TruncateProps };
@@ -0,0 +1,54 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { mergeProps } from '@base-ui/react/merge-props';
3
+ import { useRender } from '@base-ui/react/use-render';
4
+ import { Children, useRef } from 'react';
5
+ import { useIsTruncated } from '../../hooks/use-is-truncated.js';
6
+ import { cn } from '../../lib/utils.js';
7
+ import { Tooltip, TooltipContent, TooltipTrigger } from './tooltip.js';
8
+ /** Plain text of string / number children; undefined as soon as any child is an element. */
9
+ function plainText(children) {
10
+ const parts = Children.toArray(children);
11
+ return parts.every((part) => typeof part === 'string' || typeof part === 'number')
12
+ ? parts.join('')
13
+ : undefined;
14
+ }
15
+ /**
16
+ * Shared by `Truncate`, `Text ellipsis` and `Heading ellipsis`: attach your `ref` to the clipped
17
+ * element and wrap it in `TruncationTooltip` with the returned `text` and `truncated`. `tooltip`:
18
+ * `false` opts out, a string overrides the content (needed when the children are not plain text).
19
+ * `enabled: false` leaves `text` undefined, which makes the wrapper a pass-through.
20
+ */
21
+ function useTruncationTooltip(ref, enabled, tooltip, children) {
22
+ const text = enabled && tooltip !== false
23
+ ? typeof tooltip === 'string'
24
+ ? tooltip
25
+ : plainText(children)
26
+ : undefined;
27
+ const truncated = useIsTruncated(ref, text);
28
+ return { text, truncated };
29
+ }
30
+ /** Tooltip with the full text around `children`; only opens while `truncated`. */
31
+ function TruncationTooltip({ text, truncated, children, }) {
32
+ if (text === undefined)
33
+ return children;
34
+ return (_jsxs(Tooltip, { disabled: !truncated, children: [_jsx(TooltipTrigger, { render: children }), _jsx(TooltipContent, { children: text })] }));
35
+ }
36
+ /**
37
+ * Typography-free truncation: clips overflowing text with an ellipsis and shows the full text in
38
+ * a tooltip only while it is clipped. Renders a `span`; use `render` to put the same behaviour on
39
+ * another element (`render={<h1 />}`, `render={<button />}`). It adds no display or font classes,
40
+ * so the element must already be a block, flex item or `min-w-0` box that can overflow.
41
+ */
42
+ function Truncate({ className, render, tooltip = true, ref, ...props }) {
43
+ const innerRef = useRef(null);
44
+ const truncation = useTruncationTooltip(innerRef, true, tooltip, props.children);
45
+ const element = useRender({
46
+ defaultTagName: 'span',
47
+ ref: ref ? [ref, innerRef] : innerRef,
48
+ props: mergeProps({ className: cn('truncate', className) }, props),
49
+ render,
50
+ state: { slot: 'truncate' },
51
+ });
52
+ return (_jsx(TruncationTooltip, { text: truncation.text, truncated: truncation.truncated, children: element }));
53
+ }
54
+ export { Truncate, TruncationTooltip, useTruncationTooltip };
@@ -0,0 +1,55 @@
1
+ import * as React from 'react';
2
+ export interface ViewerShellLabels {
3
+ close: string;
4
+ fullscreen: string;
5
+ exitFullscreen: string;
6
+ openExternal?: string;
7
+ download?: string;
8
+ reload: string;
9
+ popOut: string;
10
+ }
11
+ /** Header zoom buttons for content the shell can't scale itself; the host owns the level. */
12
+ export interface ViewerShellZoom {
13
+ onZoomIn: () => void;
14
+ onZoomOut: () => void;
15
+ canZoomIn: boolean;
16
+ canZoomOut: boolean;
17
+ labels: {
18
+ zoomIn: string;
19
+ zoomOut: string;
20
+ };
21
+ }
22
+ export interface ViewerShellProps {
23
+ title: string;
24
+ children: React.ReactNode;
25
+ onClose: () => void;
26
+ fullscreen?: boolean;
27
+ onToggleFullscreen?: () => void;
28
+ externalHref?: string;
29
+ downloadHref?: string;
30
+ /**
31
+ * A url on an origin of its own (the user content domain) that is safe to open top-level in a
32
+ * new tab. Never pass a same-origin or storage url: the link drops `opener` and the referrer,
33
+ * but it cannot sandbox what it opens.
34
+ */
35
+ popOutHref?: string;
36
+ /**
37
+ * Runs when the pop out link is clicked, before the browser follows it. A host whose link can
38
+ * go stale (a signed url) calls `preventDefault()` and opens a fresh one itself.
39
+ */
40
+ onPopOut?: (event: React.MouseEvent<HTMLAnchorElement>) => void;
41
+ /** Re-mounts the content (and lets the host fetch a fresh signed url). */
42
+ onReload?: () => void;
43
+ zoom?: ViewerShellZoom;
44
+ /** Host chrome rendered at the very top of the card, above the header (a desktop caption band). */
45
+ lead?: React.ReactNode;
46
+ /**
47
+ * Chrome that belongs to the host, rendered under the header and outside the content: used to
48
+ * tell people that what follows is someone else's file, not the app.
49
+ */
50
+ banner?: React.ReactNode;
51
+ className?: string;
52
+ labels: ViewerShellLabels;
53
+ }
54
+ declare function ViewerShell({ title, children, onClose, fullscreen, onToggleFullscreen, externalHref, downloadHref, popOutHref, onPopOut, onReload, zoom, lead, banner, className, labels, }: ViewerShellProps): React.JSX.Element;
55
+ export { ViewerShell };
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import * as React from 'react';
3
+ import { AnimatePresence, Swap, motion } from '../../motion/index.js';
4
+ import { presets } from '../../motion/config.js';
5
+ import { cn } from '../../lib/utils.js';
6
+ import { X, Maximize, Minimize, ExternalLink, Download, Open, RotateCcw, ZoomIn, ZoomOut, } from '../../icons/index.js';
7
+ import { IconButton } from './icon-button.js';
8
+ function ViewerShell({ title, children, onClose, fullscreen = false, onToggleFullscreen, externalHref, downloadHref, popOutHref, onPopOut, onReload, zoom, lead, banner, className, labels, }) {
9
+ const handleKeyDown = (e) => {
10
+ if (e.key === 'Escape')
11
+ onClose();
12
+ };
13
+ return (_jsxs("div", { "data-slot": "viewer-shell", tabIndex: -1, onKeyDown: handleKeyDown, className: cn('flex flex-col overflow-hidden rounded-xl border bg-background shadow-lg outline-none', className), children: [lead, _jsxs("div", { className: "flex h-11 shrink-0 items-center gap-2 border-b px-3", children: [_jsx("h2", { className: "min-w-0 flex-1 truncate text-body-2 font-semibold", children: title }), _jsxs("div", { className: "flex shrink-0 items-center gap-0.5", children: [onReload ? (_jsx(IconButton, { "aria-label": labels.reload, size: "icon-sm", variant: "ghost", onClick: onReload, children: _jsx(RotateCcw, { className: "size-3.5" }) })) : null, _jsxs(AnimatePresence, { initial: false, children: [externalHref ? (_jsx(motion.a, { ...presets.pop, href: externalHref, target: "_blank", rel: "noopener noreferrer", "aria-label": labels.openExternal, className: "inline-flex size-7 items-center justify-center rounded-md text-sm text-foreground transition-colors hover:bg-accent", children: _jsx(ExternalLink, { className: "size-3.5" }) }, "external")) : null, popOutHref ? (_jsx(motion.a, { ...presets.pop, href: popOutHref, onClick: onPopOut, target: "_blank", rel: "noopener noreferrer", "aria-label": labels.popOut, className: "inline-flex size-7 items-center justify-center rounded-md text-sm text-foreground transition-colors hover:bg-accent", children: _jsx(Open, { className: "size-3.5" }) }, "pop-out")) : null, downloadHref ? (_jsx(motion.a, { ...presets.pop, href: downloadHref, download: true, "aria-label": labels.download, className: "inline-flex size-7 items-center justify-center rounded-md text-sm text-foreground transition-colors hover:bg-accent", children: _jsx(Download, { className: "size-3.5" }) }, "download")) : null] }), zoom ? (_jsxs(_Fragment, { children: [_jsx(IconButton, { "aria-label": zoom.labels.zoomOut, size: "icon-sm", variant: "ghost", disabled: !zoom.canZoomOut, onClick: zoom.onZoomOut, children: _jsx(ZoomOut, { className: "size-3.5" }) }), _jsx(IconButton, { "aria-label": zoom.labels.zoomIn, size: "icon-sm", variant: "ghost", disabled: !zoom.canZoomIn, onClick: zoom.onZoomIn, children: _jsx(ZoomIn, { className: "size-3.5" }) })] })) : null, onToggleFullscreen ? (_jsx(IconButton, { "aria-label": fullscreen ? labels.exitFullscreen : labels.fullscreen, size: "icon-sm", variant: "ghost", onClick: onToggleFullscreen, children: _jsx(Swap, { inline: true, swapKey: fullscreen ? 'minimize' : 'maximize', className: "inline-flex", children: fullscreen ? _jsx(Minimize, { className: "size-3.5" }) : _jsx(Maximize, { className: "size-3.5" }) }) })) : null, _jsx(IconButton, { "aria-label": labels.close, size: "icon-sm", variant: "ghost", onClick: onClose, children: _jsx(X, { className: "size-3.5" }) })] })] }), banner, _jsx("div", { className: "min-h-0 flex-1 overflow-auto", children: children })] }));
14
+ }
15
+ export { ViewerShell };
@@ -0,0 +1,91 @@
1
+ import * as React from 'react';
2
+ export type VirtualListHandle = {
3
+ scrollToOffset: (offset: number) => void;
4
+ /** Jumps to an item by index — has its own measurement-aware retry, so it lands correctly even
5
+ * when `estimateSize` is wrong and gets corrected by `measureElement` after mount. */
6
+ scrollToIndex: (index: number, options?: {
7
+ align?: 'start' | 'center' | 'end' | 'auto';
8
+ behavior?: 'auto' | 'smooth';
9
+ }) => void;
10
+ /** Lands on the real end (`paddingEnd` included) and tracks it while rows measure. */
11
+ scrollToEnd: (options?: {
12
+ behavior?: 'auto' | 'smooth';
13
+ }) => void;
14
+ /** Within `threshold` (default `scrollEndThreshold`) of the end. */
15
+ isAtEnd: (threshold?: number) => boolean;
16
+ /** The virtualizer's `isScrolling`, or a touch that ended moments ago. On iOS corrections wait for it. */
17
+ isScrolling: () => boolean;
18
+ /** Drops an in-flight scroll target (landing, jump) so a reader's gesture takes over. */
19
+ cancelScroll: () => void;
20
+ /** The item under `offset` (e.g. the top visible row at `scrollTop`), for key-based restore. */
21
+ getItemForOffset: (offset: number) => {
22
+ key: string;
23
+ index: number;
24
+ start: number;
25
+ } | undefined;
26
+ /** An item's start offset: measured, or estimated while it has not rendered. */
27
+ getItemStart: (index: number) => number | undefined;
28
+ /**
29
+ * Lands with the item's start `offset` px above the top edge (`getItemStart` at save time), or
30
+ * `scrollToIndex(…, { align: 'start' })` without one. Returns false if no item has that key.
31
+ */
32
+ scrollToKey: (key: string, offset?: number) => boolean;
33
+ };
34
+ export type VirtualListProps<T> = {
35
+ items: readonly T[];
36
+ estimateSize: (index: number) => number;
37
+ renderItem: (item: T, index: number) => React.ReactNode;
38
+ getKey: (item: T, index: number) => string;
39
+ /** Pixel height of the scroll container; the parent controls layout. */
40
+ height: number | string;
41
+ overscan?: number;
42
+ /**
43
+ * Keys of items that stay mounted wherever they are, even far outside the overscan. For a row
44
+ * that owns something the page cannot lose by unmounting it (a video floating in picture in
45
+ * picture). Keep the set small: each kept row is a rendered row.
46
+ */
47
+ keepMountedKeys?: ReadonlySet<string>;
48
+ /** Called when the last item enters the viewport; for reverse-chronological chat loading. */
49
+ onEndReached?: () => void;
50
+ className?: string;
51
+ /** For restoring a saved position: the virtualizer starts here, so any re-attach lands here, not 0. */
52
+ initialOffset?: number;
53
+ /** Space above the first item, known to the virtualizer (unlike CSS padding on the scroller). */
54
+ paddingStart?: number;
55
+ /** Space below the last item, known to the virtualizer. */
56
+ paddingEnd?: number;
57
+ /**
58
+ * `'end'` is TanStack's chat mode. It owns every compensating `scrollTop` write (deferred on iOS
59
+ * during touch and momentum), so nothing here writes one. On iOS a row size change while the
60
+ * list scrolls gets no compensation.
61
+ */
62
+ anchorTo?: 'start' | 'end';
63
+ /** Follow appends at the end. Also keep the end on box or `paddingEnd` changes if not moved. */
64
+ followOnAppend?: boolean;
65
+ /**
66
+ * Appended items this accepts grow into place (height 0 to natural) while the list stays on the
67
+ * end, so the rows above glide up under the reader instead of jumping by the item's height. Only
68
+ * for items that are not the reader's own send (that one is expected to land at once).
69
+ */
70
+ glideItem?: (item: T) => boolean;
71
+ /** How close to the end counts as "at the end", for following and `isAtEnd`. */
72
+ scrollEndThreshold?: number;
73
+ /** Scroll events this far apart end the virtualizer's `isScrolling` (TanStack default 150ms). */
74
+ isScrollingResetDelay?: number;
75
+ /**
76
+ * Compensate the old first row's next size change after a prepend (it can drop its header).
77
+ * TanStack skips rows in the viewport and backward scrolls. Read once per mount.
78
+ */
79
+ anchorPrependBoundary?: boolean;
80
+ /** Imperative scrolling (offset, index, key, end) and top-item lookup, for landing and restore. */
81
+ ref?: React.Ref<VirtualListHandle>;
82
+ /** Scroll this item index into view (e.g. deep-link focus). Cleared by the parent after apply. */
83
+ scrollToIndex?: number | null;
84
+ scrollToAlign?: 'start' | 'center' | 'end' | 'auto';
85
+ /** Room kept above an item scrolled to with `align: 'start'`, for chrome that floats over the top edge. */
86
+ scrollPaddingStart?: number;
87
+ /** Opt-in scroll diagnostics. Read once per mount. */
88
+ onDebug?: (event: string, context: Record<string, unknown>) => void;
89
+ };
90
+ declare function VirtualList<T>({ items, estimateSize, renderItem, getKey, height, overscan, keepMountedKeys, onEndReached, className, initialOffset, paddingStart, paddingEnd, anchorTo, followOnAppend, glideItem, scrollEndThreshold, isScrollingResetDelay, anchorPrependBoundary, ref, scrollToIndex, scrollToAlign, scrollPaddingStart, onDebug, }: VirtualListProps<T>): React.JSX.Element;
91
+ export { VirtualList };