@ui-sdk/react 0.0.0-stage → 0.1.0-beta.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 (247) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +10 -2
  3. package/dist/accordion.d.ts +331 -0
  4. package/dist/accordion.js +25 -0
  5. package/dist/agent.d.ts +2 -0
  6. package/dist/agent.js +8 -0
  7. package/dist/alert-dialog.d.ts +56 -0
  8. package/dist/alert-dialog.js +45 -0
  9. package/dist/async-options.d.ts +22 -0
  10. package/dist/async-options.js +56 -0
  11. package/dist/autocomplete.d.ts +102 -0
  12. package/dist/autocomplete.js +195 -0
  13. package/dist/avatar.d.ts +301 -0
  14. package/dist/avatar.js +17 -0
  15. package/dist/button.d.ts +21 -0
  16. package/dist/button.js +45 -0
  17. package/dist/calendar-context.d.ts +10 -0
  18. package/dist/calendar-context.js +9 -0
  19. package/dist/calendar.d.ts +80 -0
  20. package/dist/calendar.js +257 -0
  21. package/dist/carousel.d.ts +52 -0
  22. package/dist/carousel.js +151 -0
  23. package/dist/checkbox.d.ts +24 -0
  24. package/dist/checkbox.js +98 -0
  25. package/dist/children.d.ts +38 -0
  26. package/dist/children.js +63 -0
  27. package/dist/collapsible.d.ts +302 -0
  28. package/dist/collapsible.js +19 -0
  29. package/dist/color-picker.d.ts +72 -0
  30. package/dist/color-picker.js +133 -0
  31. package/dist/color.d.ts +2 -0
  32. package/dist/color.js +22 -0
  33. package/dist/combobox.d.ts +152 -0
  34. package/dist/combobox.js +338 -0
  35. package/dist/command.d.ts +88 -0
  36. package/dist/command.js +233 -0
  37. package/dist/controller-target.d.ts +3 -0
  38. package/dist/controller-target.js +59 -0
  39. package/dist/date-input.d.ts +19 -0
  40. package/dist/date-input.js +58 -0
  41. package/dist/date-picker.d.ts +47 -0
  42. package/dist/date-picker.js +310 -0
  43. package/dist/date.d.ts +2 -0
  44. package/dist/date.js +60 -0
  45. package/dist/dialog-parts.d.ts +23 -0
  46. package/dist/dialog-parts.js +142 -0
  47. package/dist/dialog.d.ts +57 -0
  48. package/dist/dialog.js +32 -0
  49. package/dist/drawer.d.ts +70 -0
  50. package/dist/drawer.js +158 -0
  51. package/dist/editor-context.d.ts +4 -0
  52. package/dist/editor-context.js +9 -0
  53. package/dist/field-context.d.ts +47 -0
  54. package/dist/field-context.js +87 -0
  55. package/dist/field.d.ts +49 -0
  56. package/dist/field.js +169 -0
  57. package/dist/file-input.d.ts +46 -0
  58. package/dist/file-input.js +170 -0
  59. package/dist/form-inputs.d.ts +5 -0
  60. package/dist/form-inputs.js +22 -0
  61. package/dist/gesture.d.ts +1 -0
  62. package/dist/gesture.js +16 -0
  63. package/dist/history.d.ts +2 -0
  64. package/dist/history.js +12 -0
  65. package/dist/hover-card.d.ts +33 -0
  66. package/dist/hover-card.js +84 -0
  67. package/dist/image-cropper.d.ts +333 -0
  68. package/dist/image-cropper.js +37 -0
  69. package/dist/image.d.ts +2 -0
  70. package/dist/image.js +6 -0
  71. package/dist/input.d.ts +22 -0
  72. package/dist/input.js +51 -0
  73. package/dist/label.d.ts +294 -0
  74. package/dist/label.js +9 -0
  75. package/dist/list-items.d.ts +15 -0
  76. package/dist/list-items.js +39 -0
  77. package/dist/list-text.d.ts +4 -0
  78. package/dist/list-text.js +15 -0
  79. package/dist/listbox.d.ts +56 -0
  80. package/dist/listbox.js +122 -0
  81. package/dist/menu.d.ts +116 -0
  82. package/dist/menu.js +426 -0
  83. package/dist/menubar-context.d.ts +4 -0
  84. package/dist/menubar-context.js +7 -0
  85. package/dist/menubar.d.ts +11 -0
  86. package/dist/menubar.js +32 -0
  87. package/dist/meter.d.ts +312 -0
  88. package/dist/meter.js +30 -0
  89. package/dist/navigation-menu.d.ts +61 -0
  90. package/dist/navigation-menu.js +169 -0
  91. package/dist/number-input.d.ts +44 -0
  92. package/dist/number-input.js +185 -0
  93. package/dist/number.d.ts +2 -0
  94. package/dist/number.js +40 -0
  95. package/dist/observe.d.ts +2 -0
  96. package/dist/observe.js +8 -0
  97. package/dist/otp-input.d.ts +29 -0
  98. package/dist/otp-input.js +94 -0
  99. package/dist/overlays.d.ts +26 -0
  100. package/dist/overlays.js +82 -0
  101. package/dist/pagination.d.ts +336 -0
  102. package/dist/pagination.js +71 -0
  103. package/dist/password-input.d.ts +23 -0
  104. package/dist/password-input.js +65 -0
  105. package/dist/popover.d.ts +54 -0
  106. package/dist/popover.js +151 -0
  107. package/dist/popup.d.ts +41 -0
  108. package/dist/popup.js +180 -0
  109. package/dist/preferences.d.ts +35 -0
  110. package/dist/preferences.js +93 -0
  111. package/dist/primitive.d.ts +2 -0
  112. package/dist/primitive.js +14 -0
  113. package/dist/progress.d.ts +312 -0
  114. package/dist/progress.js +30 -0
  115. package/dist/radio.d.ts +22 -0
  116. package/dist/radio.js +86 -0
  117. package/dist/rating.d.ts +20 -0
  118. package/dist/rating.js +94 -0
  119. package/dist/render-element.d.ts +35 -0
  120. package/dist/render-element.js +103 -0
  121. package/dist/resizable.d.ts +302 -0
  122. package/dist/resizable.js +23 -0
  123. package/dist/scroll-area.d.ts +324 -0
  124. package/dist/scroll-area.js +49 -0
  125. package/dist/segmented-control.d.ts +24 -0
  126. package/dist/segmented-control.js +78 -0
  127. package/dist/segmented-input.d.ts +15 -0
  128. package/dist/segmented-input.js +48 -0
  129. package/dist/select.d.ts +125 -0
  130. package/dist/select.js +330 -0
  131. package/dist/separator.d.ts +290 -0
  132. package/dist/separator.js +9 -0
  133. package/dist/shortcuts.d.ts +32 -0
  134. package/dist/shortcuts.js +61 -0
  135. package/dist/slider.d.ts +38 -0
  136. package/dist/slider.js +163 -0
  137. package/dist/switch.d.ts +15 -0
  138. package/dist/switch.js +39 -0
  139. package/dist/tabs.d.ts +331 -0
  140. package/dist/tabs.js +28 -0
  141. package/dist/text-control.d.ts +22 -0
  142. package/dist/text-control.js +24 -0
  143. package/dist/text-match.d.ts +2 -0
  144. package/dist/text-match.js +8 -0
  145. package/dist/textarea.d.ts +10 -0
  146. package/dist/textarea.js +48 -0
  147. package/dist/time-input.d.ts +19 -0
  148. package/dist/time-input.js +63 -0
  149. package/dist/timeline.d.ts +327 -0
  150. package/dist/timeline.js +31 -0
  151. package/dist/toast-parts.d.ts +63 -0
  152. package/dist/toast-parts.js +144 -0
  153. package/dist/toast.d.ts +2 -0
  154. package/dist/toast.js +22 -0
  155. package/dist/toggle.d.ts +23 -0
  156. package/dist/toggle.js +76 -0
  157. package/dist/token-input.d.ts +35 -0
  158. package/dist/token-input.js +100 -0
  159. package/dist/toolbar-context.d.ts +6 -0
  160. package/dist/toolbar-context.js +16 -0
  161. package/dist/toolbar.d.ts +38 -0
  162. package/dist/toolbar.js +63 -0
  163. package/dist/tooltip.d.ts +39 -0
  164. package/dist/tooltip.js +108 -0
  165. package/dist/tree.d.ts +51 -0
  166. package/dist/tree.js +150 -0
  167. package/dist/trigger-menu.d.ts +83 -0
  168. package/dist/trigger-menu.js +167 -0
  169. package/dist/use-primitive.d.ts +178 -0
  170. package/dist/use-primitive.js +674 -0
  171. package/dist/waterfall.d.ts +80 -0
  172. package/dist/waterfall.js +108 -0
  173. package/docs/README.md +89 -0
  174. package/docs/agents.md +1523 -0
  175. package/docs/components/accordion.md +1391 -0
  176. package/docs/components/alert-dialog.md +850 -0
  177. package/docs/components/autocomplete.md +1963 -0
  178. package/docs/components/avatar.md +252 -0
  179. package/docs/components/button.md +444 -0
  180. package/docs/components/calendar.md +1687 -0
  181. package/docs/components/carousel.md +1244 -0
  182. package/docs/components/checkbox.md +1069 -0
  183. package/docs/components/collapsible.md +567 -0
  184. package/docs/components/color-picker.md +736 -0
  185. package/docs/components/combobox.md +3647 -0
  186. package/docs/components/command.md +1573 -0
  187. package/docs/components/date-input.md +608 -0
  188. package/docs/components/date-picker.md +1266 -0
  189. package/docs/components/dialog.md +1469 -0
  190. package/docs/components/drawer.md +1197 -0
  191. package/docs/components/field.md +549 -0
  192. package/docs/components/file-input.md +609 -0
  193. package/docs/components/hover-card.md +514 -0
  194. package/docs/components/image-cropper.md +579 -0
  195. package/docs/components/input.md +301 -0
  196. package/docs/components/label.md +217 -0
  197. package/docs/components/listbox.md +1130 -0
  198. package/docs/components/menu.md +1286 -0
  199. package/docs/components/menubar.md +455 -0
  200. package/docs/components/meter.md +304 -0
  201. package/docs/components/navigation-menu.md +1581 -0
  202. package/docs/components/number-input.md +1086 -0
  203. package/docs/components/otp-input.md +565 -0
  204. package/docs/components/pagination.md +648 -0
  205. package/docs/components/password-input.md +737 -0
  206. package/docs/components/popover.md +1552 -0
  207. package/docs/components/preferences.md +220 -0
  208. package/docs/components/progress.md +443 -0
  209. package/docs/components/radio.md +846 -0
  210. package/docs/components/rating.md +458 -0
  211. package/docs/components/resizable.md +982 -0
  212. package/docs/components/scroll-area.md +980 -0
  213. package/docs/components/segmented-control.md +465 -0
  214. package/docs/components/select.md +2133 -0
  215. package/docs/components/separator.md +243 -0
  216. package/docs/components/slider.md +1169 -0
  217. package/docs/components/switch.md +613 -0
  218. package/docs/components/tabs.md +1014 -0
  219. package/docs/components/textarea.md +318 -0
  220. package/docs/components/time-input.md +231 -0
  221. package/docs/components/timeline.md +677 -0
  222. package/docs/components/toast.md +1395 -0
  223. package/docs/components/toggle.md +541 -0
  224. package/docs/components/token-input.md +1007 -0
  225. package/docs/components/toolbar.md +787 -0
  226. package/docs/components/tooltip.md +884 -0
  227. package/docs/components/tree.md +1346 -0
  228. package/docs/components/trigger-menu.md +1257 -0
  229. package/docs/components/waterfall.md +754 -0
  230. package/docs/controllers.md +89 -0
  231. package/docs/core.md +8027 -0
  232. package/docs/custom-parts.md +120 -0
  233. package/docs/first-component.md +83 -0
  234. package/docs/forms.md +148 -0
  235. package/docs/hooks/async-options.md +366 -0
  236. package/docs/hooks/shortcuts.md +221 -0
  237. package/docs/installation.md +45 -0
  238. package/docs/introduction.md +32 -0
  239. package/docs/lists-and-menus.md +170 -0
  240. package/docs/llms.txt +1844 -0
  241. package/docs/parts-and-composition.md +93 -0
  242. package/docs/popups.md +117 -0
  243. package/docs/rendering.md +121 -0
  244. package/docs/server-rendering.md +253 -0
  245. package/docs/state-and-events.md +105 -0
  246. package/docs/styling-and-animation.md +176 -0
  247. package/package.json +333 -3
@@ -0,0 +1,754 @@
1
+ # Waterfall
2
+
3
+ Show a trace or a page's requests as bars on a shared time axis, with spans that expand to show what they started.
4
+
5
+ Use a Waterfall for anything with a start and a duration that nests: a browser's network requests, an OpenTelemetry trace, the steps of a build. Each span is a row with its own bar, placed on one time axis, and screen reader and keyboard users get a treegrid they can walk span by span.
6
+
7
+ ```tsx title="network.tsx"
8
+ "use client"
9
+
10
+ import {
11
+ Waterfall,
12
+ WaterfallAxis,
13
+ WaterfallBar,
14
+ WaterfallCell,
15
+ WaterfallColumnHeader,
16
+ WaterfallHeaderRow,
17
+ WaterfallMark,
18
+ WaterfallRow,
19
+ WaterfallRowHeader,
20
+ type WaterfallData,
21
+ } from "@ui-sdk/react/waterfall"
22
+
23
+ const requests: WaterfallData = [
24
+ { value: "document", label: "/", start: 0, duration: 182 },
25
+ { value: "styles", label: "app.css", start: 190, duration: 96 },
26
+ { value: "script", label: "app.js", start: 192, duration: 410 },
27
+ { value: "font", label: "inter.woff2", start: 300, duration: 220 },
28
+ { value: "user", label: "api/user", start: 610, duration: 240 },
29
+ { value: "avatar", label: "avatar.png", start: 860, duration: 150 },
30
+ ]
31
+
32
+ const sizes: Record<string, string> = {
33
+ document: "14 kB",
34
+ styles: "8 kB",
35
+ script: "212 kB",
36
+ font: "48 kB",
37
+ user: "1 kB",
38
+ avatar: "36 kB",
39
+ }
40
+
41
+ function Example() {
42
+ return (
43
+ <Waterfall
44
+ aria-label="Network requests"
45
+ items={requests}
46
+ getBarLabel={(request, { start, duration }) =>
47
+ `${sizes[request.value]}, starts at ${start}, takes ${duration}`
48
+ }
49
+ >
50
+ <WaterfallHeaderRow>
51
+ <WaterfallColumnHeader>Name</WaterfallColumnHeader>
52
+ <WaterfallColumnHeader>Size</WaterfallColumnHeader>
53
+ <WaterfallColumnHeader aria-label="Timeline">
54
+ <WaterfallAxis maxTicks={5} />
55
+ <WaterfallMark time={620}>DOMContentLoaded</WaterfallMark>
56
+ </WaterfallColumnHeader>
57
+ </WaterfallHeaderRow>
58
+ {requests.map((request) => (
59
+ <WaterfallRow key={request.value} value={request.value}>
60
+ <WaterfallRowHeader>{request.label}</WaterfallRowHeader>
61
+ <WaterfallCell>{sizes[request.value]}</WaterfallCell>
62
+ <WaterfallBar />
63
+ </WaterfallRow>
64
+ ))}
65
+ </Waterfall>
66
+ )
67
+ }
68
+
69
+ export { Example }
70
+ ```
71
+
72
+ Use the arrow keys to move between requests. Press Right to move into a row's cells, where each bar reads out when its request starts and how long it takes.
73
+
74
+ ## Installation
75
+
76
+ ```bash
77
+ npm install @ui-sdk/react @ui-sdk/core
78
+ ```
79
+
80
+ ## Composition
81
+
82
+ Use the following composition to build a `Waterfall`:
83
+
84
+ ```text
85
+ Waterfall
86
+ ├── WaterfallHeaderRow
87
+ │ ├── WaterfallColumnHeader
88
+ │ └── WaterfallColumnHeader
89
+ │ ├── WaterfallAxis
90
+ │ │ └── WaterfallTick
91
+ │ └── WaterfallMark
92
+ ├── WaterfallRow
93
+ │ ├── WaterfallRowHeader
94
+ │ │ └── WaterfallIndicator
95
+ │ ├── WaterfallCell
96
+ │ └── WaterfallBar
97
+ └── WaterfallRow
98
+ ├── WaterfallRowHeader
99
+ └── WaterfallBar
100
+ ```
101
+
102
+ Pass every span as `items` and render a `WaterfallRow` for each with the span's `value`. The rows are flat siblings: the waterfall reads each one's level, place among its siblings and children from `items`. Give the waterfall an accessible name.
103
+
104
+ `WaterfallRowHeader` names the span, and holds the `WaterfallIndicator` of a span with children. `WaterfallBar` is the cell on the time axis. `WaterfallCell` is any other column, like a status or a size. The header row, the axis and marks are optional.
105
+
106
+ ## Usage
107
+
108
+ Each span has a `value`, a `label`, a `start` and a `duration` in milliseconds, and the spans it started as `children`:
109
+
110
+ ```tsx
111
+ import {
112
+ Waterfall,
113
+ WaterfallBar,
114
+ WaterfallRow,
115
+ WaterfallRowHeader,
116
+ type WaterfallData,
117
+ } from "@ui-sdk/react/waterfall"
118
+
119
+ const requests: WaterfallData = [
120
+ { value: "document", label: "/", start: 0, duration: 182 },
121
+ { value: "script", label: "app.js", start: 192, duration: 410 },
122
+ ]
123
+
124
+ function Example() {
125
+ return (
126
+ <Waterfall aria-label="Network requests" items={requests}>
127
+ {requests.map((request) => (
128
+ <WaterfallRow key={request.value} value={request.value}>
129
+ <WaterfallRowHeader>{request.label}</WaterfallRowHeader>
130
+ <WaterfallBar />
131
+ </WaterfallRow>
132
+ ))}
133
+ </Waterfall>
134
+ )
135
+ }
136
+
137
+ export { Example }
138
+ ```
139
+
140
+ Every row sets `--waterfall-start` and `--waterfall-size`, where its span begins and how much of the axis it covers, as percentages. Place the bar with them in plain CSS, with no layout in JavaScript:
141
+
142
+ ```css
143
+ [data-slot="waterfall-bar"] {
144
+ position: relative;
145
+ }
146
+
147
+ [data-slot="waterfall-bar"]::before {
148
+ content: "";
149
+ position: absolute;
150
+ inset-block: 6px;
151
+ inset-inline-start: var(--waterfall-start);
152
+ width: var(--waterfall-size);
153
+ background: steelblue;
154
+ }
155
+ ```
156
+
157
+ Without children, the waterfall renders a row for every span, with its header and bar.
158
+
159
+ ## Network Requests
160
+
161
+ The first example adds a header row. `WaterfallAxis` renders a `WaterfallTick` at round times, at most `maxTicks` of them, and `WaterfallMark` draws a moment across every row, like DOMContentLoaded, placed by `--waterfall-mark`. The axis is hidden from screen readers, who hear each bar's own times instead. `getBarLabel` changes what a bar says; this one adds the request's size.
162
+
163
+ ## Nested Spans
164
+
165
+ Spans with `children` expand and collapse. Click a span's indicator, or press Right and Left on its row. To render only the rows that show, keep `expanded` in state and pass it to `visibleWaterfallRows(items, expanded)`, which flattens the open spans in order.
166
+
167
+ Clicking a row, or pressing Enter or Space on it, selects it. `value` and `onValueChange` hold the selected span, like the one a details panel shows. `format` changes how times read, here as narrow milliseconds.
168
+
169
+ ```tsx title="trace.tsx"
170
+ "use client"
171
+
172
+ import {
173
+ createWaterfallController,
174
+ visibleWaterfallRows,
175
+ Waterfall,
176
+ WaterfallAxis,
177
+ WaterfallBar,
178
+ WaterfallColumnHeader,
179
+ WaterfallHeaderRow,
180
+ WaterfallIndicator,
181
+ WaterfallRow,
182
+ WaterfallRowHeader,
183
+ type WaterfallData,
184
+ } from "@ui-sdk/react/waterfall"
185
+ import { ChevronRightIcon } from "lucide-react"
186
+ import * as React from "react"
187
+ import { useLog } from "./log"
188
+
189
+ const trace: WaterfallData = [
190
+ {
191
+ value: "checkout",
192
+ label: "POST /checkout",
193
+ start: 0,
194
+ duration: 920,
195
+ children: [
196
+ { value: "auth", label: "auth.verify", start: 12, duration: 64 },
197
+ {
198
+ value: "cart",
199
+ label: "cart.load",
200
+ start: 80,
201
+ duration: 310,
202
+ children: [
203
+ {
204
+ value: "cart-db",
205
+ label: "db.query carts",
206
+ start: 96,
207
+ duration: 140,
208
+ },
209
+ {
210
+ value: "prices",
211
+ label: "pricing.quote",
212
+ start: 240,
213
+ duration: 136,
214
+ },
215
+ ],
216
+ },
217
+ {
218
+ value: "payment",
219
+ label: "payments.charge",
220
+ start: 400,
221
+ duration: 470,
222
+ children: [
223
+ { value: "fraud", label: "fraud.score", start: 410, duration: 120 },
224
+ {
225
+ value: "gateway",
226
+ label: "gateway.capture",
227
+ start: 540,
228
+ duration: 310,
229
+ },
230
+ ],
231
+ },
232
+ { value: "email", label: "email.enqueue", start: 880, duration: 30 },
233
+ ],
234
+ },
235
+ ]
236
+
237
+ const milliseconds = {
238
+ style: "unit",
239
+ unit: "millisecond",
240
+ unitDisplay: "narrow",
241
+ } as const
242
+
243
+ function Example() {
244
+ const log = useLog()
245
+ const [spans] = React.useState(() => createWaterfallController())
246
+ const [expanded, setExpanded] = React.useState(["checkout"])
247
+ const [value, setValue] = React.useState<string | null>(null)
248
+ const rows = visibleWaterfallRows(trace, expanded)
249
+ return (
250
+ <div data-slot="stack">
251
+ <div data-slot="controls">
252
+ <button type="button" onClick={() => spans.expandAll()}>
253
+ Expand All
254
+ </button>
255
+ <button type="button" onClick={() => spans.collapseAll()}>
256
+ Collapse All
257
+ </button>
258
+ </div>
259
+ <Waterfall
260
+ aria-label="Checkout trace"
261
+ items={trace}
262
+ controller={spans}
263
+ expanded={expanded}
264
+ onExpandedChange={setExpanded}
265
+ value={value}
266
+ onValueChange={(next) => {
267
+ setValue(next)
268
+ log(`Selected ${next}`)
269
+ }}
270
+ format={milliseconds}
271
+ >
272
+ <WaterfallHeaderRow>
273
+ <WaterfallColumnHeader>Span</WaterfallColumnHeader>
274
+ <WaterfallColumnHeader aria-label="Timeline">
275
+ <WaterfallAxis />
276
+ </WaterfallColumnHeader>
277
+ </WaterfallHeaderRow>
278
+ {rows.map(({ item }) => (
279
+ <WaterfallRow key={item.value} value={item.value}>
280
+ <WaterfallRowHeader>
281
+ {item.children && (
282
+ <WaterfallIndicator>
283
+ <ChevronRightIcon aria-hidden="true" />
284
+ </WaterfallIndicator>
285
+ )}
286
+ {item.label}
287
+ </WaterfallRowHeader>
288
+ <WaterfallBar />
289
+ </WaterfallRow>
290
+ ))}
291
+ </Waterfall>
292
+ </div>
293
+ )
294
+ }
295
+
296
+ export { Example }
297
+ ```
298
+
299
+ Expand payments.charge with Right, then press Enter on gateway.capture. The console logs the selected span. Collapse All closes every span.
300
+
301
+ You can also render every row and let the waterfall hide the ones inside a collapsed span: rows it hides get `hidden`, so `expanded` can stay uncontrolled.
302
+
303
+ ## Zoom
304
+
305
+ `range` sets the stretch of time the axis shows, as `[start, end]`. Change it to zoom or pan: the bars and ticks follow, and a bar outside the range shrinks to its edge. Without it, the axis runs from the earliest start to the latest end.
306
+
307
+ `WaterfallAxis` also takes a function of its `ticks`, for ticks of your own.
308
+
309
+ ```tsx title="zoom.tsx"
310
+ "use client"
311
+
312
+ import {
313
+ Waterfall,
314
+ WaterfallAxis,
315
+ WaterfallBar,
316
+ WaterfallColumnHeader,
317
+ WaterfallHeaderRow,
318
+ WaterfallIndicator,
319
+ WaterfallRow,
320
+ WaterfallRowHeader,
321
+ WaterfallTick,
322
+ type WaterfallData,
323
+ } from "@ui-sdk/react/waterfall"
324
+ import { ChevronRightIcon } from "lucide-react"
325
+ import * as React from "react"
326
+
327
+ const trace: WaterfallData = [
328
+ {
329
+ value: "render",
330
+ label: "render /dashboard",
331
+ start: 0,
332
+ duration: 1200,
333
+ children: [
334
+ { value: "session", label: "session", start: 20, duration: 80 },
335
+ { value: "metrics", label: "metrics", start: 110, duration: 640 },
336
+ { value: "alerts", label: "alerts", start: 110, duration: 260 },
337
+ { value: "html", label: "stream html", start: 760, duration: 420 },
338
+ ],
339
+ },
340
+ ]
341
+
342
+ const spans = [trace[0], ...trace[0].children!]
343
+ const full: [number, number] = [0, 1200]
344
+
345
+ function zoomed([start, end]: [number, number], factor: number) {
346
+ const center = (start + end) / 2
347
+ const half = Math.min(((end - start) * factor) / 2, full[1] / 2)
348
+ const from = Math.min(Math.max(full[0], center - half), full[1] - half * 2)
349
+ return [from, from + half * 2] as [number, number]
350
+ }
351
+
352
+ function Example() {
353
+ const [range, setRange] = React.useState(full)
354
+ return (
355
+ <div data-slot="stack">
356
+ <div data-slot="controls">
357
+ <button type="button" onClick={() => setRange(zoomed(range, 0.5))}>
358
+ Zoom In
359
+ </button>
360
+ <button type="button" onClick={() => setRange(zoomed(range, 2))}>
361
+ Zoom Out
362
+ </button>
363
+ <button type="button" onClick={() => setRange(full)}>
364
+ Reset
365
+ </button>
366
+ </div>
367
+ <Waterfall
368
+ aria-label="Dashboard render"
369
+ items={trace}
370
+ range={range}
371
+ defaultExpanded={["render"]}
372
+ defaultValue="metrics"
373
+ >
374
+ <WaterfallHeaderRow>
375
+ <WaterfallColumnHeader>Span</WaterfallColumnHeader>
376
+ <WaterfallColumnHeader aria-label="Timeline">
377
+ <WaterfallAxis>
378
+ {({ ticks }) =>
379
+ ticks.map((tick) => (
380
+ <WaterfallTick key={tick.time} time={tick.time}>
381
+ {tick.label}
382
+ </WaterfallTick>
383
+ ))
384
+ }
385
+ </WaterfallAxis>
386
+ </WaterfallColumnHeader>
387
+ </WaterfallHeaderRow>
388
+ {spans.map((span) => (
389
+ <WaterfallRow key={span.value} value={span.value}>
390
+ <WaterfallRowHeader>
391
+ {span.children && (
392
+ <WaterfallIndicator>
393
+ <ChevronRightIcon aria-hidden="true" />
394
+ </WaterfallIndicator>
395
+ )}
396
+ {span.label}
397
+ </WaterfallRowHeader>
398
+ <WaterfallBar />
399
+ </WaterfallRow>
400
+ ))}
401
+ </Waterfall>
402
+ </div>
403
+ )
404
+ }
405
+
406
+ export { Example }
407
+ ```
408
+
409
+ Press Zoom In twice. The axis shows smaller steps, and the metrics span stretches past the edges.
410
+
411
+ ## Large Traces
412
+
413
+ A trace can have thousands of spans. Render only the rows in view: flatten the open spans with `visibleWaterfallRows`, and render the nearby rows at their offsets. The waterfall still reports every row's level and place among its siblings, so screen readers hear the whole hierarchy. Give the waterfall `aria-rowcount` with the number of rows that show, and each row `aria-rowindex` with its index plus one, so a screen reader counts rows in the whole trace, not just the ones in the page.
414
+
415
+ `onHighlight` reports the index of the row the keyboard moves to. Scroll it into view there, so a row that isn't rendered yet takes focus once it is. An arrow key changes only the two rows it moves between, however long the trace is.
416
+
417
+ ```tsx title="virtualized.tsx"
418
+ "use client"
419
+
420
+ import {
421
+ visibleWaterfallRows,
422
+ Waterfall,
423
+ WaterfallBar,
424
+ WaterfallIndicator,
425
+ WaterfallRow,
426
+ WaterfallRowHeader,
427
+ type WaterfallData,
428
+ } from "@ui-sdk/react/waterfall"
429
+ import { ChevronRightIcon } from "lucide-react"
430
+ import * as React from "react"
431
+
432
+ const trace: WaterfallData = Array.from({ length: 2000 }, (_, job) => ({
433
+ value: `job-${job + 1}`,
434
+ label: `job ${job + 1}`,
435
+ start: job * 4,
436
+ duration: 60,
437
+ children: Array.from({ length: 4 }, (_, step) => ({
438
+ value: `job-${job + 1}/step-${step + 1}`,
439
+ label: `step ${step + 1}`,
440
+ start: job * 4 + step * 15,
441
+ duration: 15,
442
+ })),
443
+ }))
444
+
445
+ const range: [number, number] = [0, 400]
446
+ const row = 32
447
+ const height = 288
448
+
449
+ function Example() {
450
+ const scroller = React.useRef<HTMLDivElement>(null)
451
+ const [scrollTop, setScrollTop] = React.useState(0)
452
+ const [expanded, setExpanded] = React.useState(["job-1"])
453
+ const rows = React.useMemo(
454
+ () => visibleWaterfallRows(trace, expanded),
455
+ [expanded]
456
+ )
457
+ const start = Math.max(0, Math.floor(scrollTop / row) - 4)
458
+ const end = Math.min(rows.length, Math.ceil((scrollTop + height) / row) + 4)
459
+ return (
460
+ <div
461
+ data-slot="scroll"
462
+ ref={scroller}
463
+ onScroll={(event) => setScrollTop(event.currentTarget.scrollTop)}
464
+ >
465
+ <Waterfall
466
+ aria-label="Build trace"
467
+ aria-rowcount={rows.length}
468
+ items={trace}
469
+ range={range}
470
+ expanded={expanded}
471
+ onExpandedChange={setExpanded}
472
+ onHighlight={(_value, { index }) => {
473
+ const el = scroller.current
474
+ if (!el) {
475
+ return
476
+ }
477
+ const top = index * row
478
+ if (top < el.scrollTop) {
479
+ el.scrollTop = top
480
+ } else if (top + row > el.scrollTop + el.clientHeight) {
481
+ el.scrollTop = top + row - el.clientHeight
482
+ }
483
+ setScrollTop(el.scrollTop)
484
+ }}
485
+ style={{ height: rows.length * row }}
486
+ >
487
+ {rows.slice(start, end).map(({ item, index }) => (
488
+ <WaterfallRow
489
+ key={item.value}
490
+ value={item.value}
491
+ aria-rowindex={index + 1}
492
+ style={{ top: index * row }}
493
+ >
494
+ <WaterfallRowHeader>
495
+ {item.children && (
496
+ <WaterfallIndicator>
497
+ <ChevronRightIcon aria-hidden="true" />
498
+ </WaterfallIndicator>
499
+ )}
500
+ {item.label}
501
+ </WaterfallRowHeader>
502
+ <WaterfallBar />
503
+ </WaterfallRow>
504
+ ))}
505
+ </Waterfall>
506
+ </div>
507
+ )
508
+ }
509
+
510
+ export { Example }
511
+ ```
512
+
513
+ Press End to focus job 2000, or Page Down to move a page at a time. The trace has 10,000 spans, but only nearby rows render.
514
+
515
+ ## Controlled
516
+
517
+ Selection and expanded spans are independent:
518
+
519
+ | Value | Shape | Change Notification |
520
+ | ---------- | ------------------------ | ------------------- |
521
+ | `value` | A string or `null`. | `onValueChange` |
522
+ | `expanded` | An array of span values. | `onExpandedChange` |
523
+
524
+ Supply a value and its change callback to control it, or use `defaultValue` and `defaultExpanded` for starting state. Call `details.cancel()` in a change callback to reject a change. [State and Events](../state-and-events.md) covers the shared binding pattern.
525
+
526
+ ## Controller
527
+
528
+ A `createWaterfallController()` instance can expand or collapse spans, change the selection, or focus a row. `reveal(value)` opens a span's ancestors and focuses its row. The Nested Spans example uses one for Expand All and Collapse All. See [Controllers](../controllers.md) for setup and lifecycle.
529
+
530
+ ## Custom Rendering
531
+
532
+ Every part takes `render`, and class names, styles and children that are functions of its state. See [Parts and Composition](../parts-and-composition.md) for prop merging and state-based rendering.
533
+
534
+ ## Styling
535
+
536
+ Style a row's state from its `data-selected`, `data-expanded`, `data-branch` and `data-level`, which its header, indicator, cells and bar carry too. Focus moves between rows and cells, so style a focused one with `:focus-visible`. Indent a row's header with `--waterfall-level`, and place its bar with `--waterfall-start` and `--waterfall-size`.
537
+
538
+ Each part has a `data-slot`, such as `waterfall-row`, and its state attributes are listed under [Data Attributes](#data-attributes).
539
+
540
+ ### WaterfallTick
541
+ - `--waterfall-tick`: Where the tick's time sits on the axis, as a percentage. Use it for `inset-inline-start`.
542
+
543
+ ### WaterfallMark
544
+ - `--waterfall-mark`: Where the mark's time sits on the axis, as a percentage. Use it for `inset-inline-start`.
545
+
546
+ ### WaterfallRow
547
+ - `--waterfall-level`: The span's depth, from 1 at the top level. Use it to indent the row's header.
548
+ - `--waterfall-start`: Where the span starts on the axis, as a percentage, clamped to the range. Use it for the bar's `inset-inline-start`.
549
+ - `--waterfall-size`: How much of the axis the span covers, as a percentage. Use it for the bar's width. Zero for a span outside the range.
550
+
551
+ ## Data Attributes
552
+
553
+ ### WaterfallMark
554
+ `[data-slot="waterfall-mark"]`
555
+
556
+ - `data-outside`: Present while the mark's time is outside the range, so it doesn't show on the axis.
557
+
558
+ ### WaterfallRow
559
+ `[data-slot="waterfall-row"]`
560
+
561
+ - `data-level` (`1`, `2`): The span's depth, from 1 at the top level. Also in the `--waterfall-level` variable.
562
+ - `data-branch`: Present on a span with children.
563
+ - `data-expanded`: Present while a span with children is expanded.
564
+ - `data-selected`: Present on the selected span's row and its parts.
565
+
566
+ ### WaterfallRowHeader
567
+ `[data-slot="waterfall-row-header"]`
568
+
569
+ - `data-level` (`1`, `2`): The span's depth, from 1 at the top level. Also in the `--waterfall-level` variable.
570
+ - `data-branch`: Present on a span with children.
571
+ - `data-expanded`: Present while a span with children is expanded.
572
+ - `data-selected`: Present on the selected span's row and its parts.
573
+
574
+ ### WaterfallIndicator
575
+ `[data-slot="waterfall-indicator"]`
576
+
577
+ - `data-level` (`1`): The span's depth, from 1 at the top level. Also in the `--waterfall-level` variable.
578
+ - `data-branch`: Present on a span with children.
579
+ - `data-expanded`: Present while a span with children is expanded.
580
+
581
+ ### WaterfallCell
582
+ `[data-slot="waterfall-cell"]`
583
+
584
+ - `data-level` (`1`, `2`): The span's depth, from 1 at the top level. Also in the `--waterfall-level` variable.
585
+ - `data-branch`: Present on a span with children.
586
+ - `data-expanded`: Present while a span with children is expanded.
587
+ - `data-selected`: Present on the selected span's row and its parts.
588
+
589
+ ### WaterfallBar
590
+ `[data-slot="waterfall-bar"]`
591
+
592
+ - `data-level` (`1`, `2`): The span's depth, from 1 at the top level. Also in the `--waterfall-level` variable.
593
+ - `data-branch`: Present on a span with children.
594
+ - `data-expanded`: Present while a span with children is expanded.
595
+ - `data-selected`: Present on the selected span's row and its parts.
596
+
597
+ ## API Reference
598
+
599
+ ### Waterfall
600
+
601
+ - `items` (`WaterfallData`): The spans, each with a `value`, a `label`, a `start`, a `duration` and the spans it started as `children`.
602
+ - `range?` (`[number, number]`): The stretch of time the axis shows, as `[start, end]` in milliseconds. Bars outside it shrink to its edges. Change it to zoom or pan.
603
+ - `value?` (`string | null`): The selected span's value. Pass it to control the selection. `null` selects nothing.
604
+ - `defaultValue?` (`string | null`): The span selected at first, when uncontrolled.
605
+ - `onValueChange?` (`((value: string | null, details: ChangeDetails<WaterfallValueChangeReason>) => void)`): Runs when the selection wants to change. `details.reason` says why, and `details.cancel()` stops it.
606
+ - `expanded?` (`string[]`): The expanded spans' values. Pass it to control which spans show their children.
607
+ - `defaultExpanded?` (`string[]`): The spans expanded at first, when uncontrolled.
608
+ - `onExpandedChange?` (`((expanded: string[], details: ChangeDetails<WaterfallExpandedChangeReason>) => void)`): Runs when spans want to expand or collapse. `details.reason` says why, and `details.cancel()` stops it.
609
+ - `onHighlight?` (`((value: string, details: ListHighlightDetails) => void)`): Runs when the keyboard moves focus to a row, with its place among the rows that show, from 0. A waterfall that renders only the rows in view scrolls that row into view here, so it can take focus once it renders.
610
+ - `format?` (`NumberFormat`): How times read on the axis and to screen readers: `Intl.NumberFormat` options for a number of milliseconds, or a function of it.
611
+ - `locale?` (`string`): The locale times are formatted in. Defaults to the page's `lang`, or the browser's language without one, once the page is interactive. Server markup uses English until then, so it never disagrees with the client.
612
+ - `getBarLabel?` (`((item: WaterfallDataItem, details: WaterfallBarLabelDetails) => string)`): What screen readers hear for a span's bar. Set it in the page's language.
613
+ - `controller?` (`object`): Drives the waterfall from anywhere. Create one with `createWaterfallController()`.
614
+ - `debug?` (`boolean`): Logs every state change with its reason to the console, in development builds.
615
+
616
+ ### WaterfallAxis
617
+
618
+ - `maxTicks?` (`number`): The most ticks the axis shows. Ticks fall on round times, so it may show fewer.
619
+
620
+ ### WaterfallTick and WaterfallMark
621
+
622
+ - `time` (`number`): The moment it marks, in milliseconds.
623
+
624
+ ### WaterfallRow
625
+
626
+ - `value` (`string`): The value of the span the row shows.
627
+
628
+ ### Items as Data
629
+
630
+ Each span in `items`:
631
+
632
+ - `value` (`string`): Identifies the span. Unique in the waterfall.
633
+ - `label` (`string`): What its row shows, and what typeahead matches.
634
+ - `start` (`number`): When it starts, in milliseconds, on the same clock as every other span.
635
+ - `duration` (`number`): How long it lasts, in milliseconds.
636
+ - `children?` (`WaterfallDataItem[]`): The spans it started. Leave it out for a span with none.
637
+
638
+ Each row `visibleWaterfallRows(items, expanded)` returns:
639
+
640
+ - `item` (`WaterfallDataItem`): The span this row shows, the same object you passed in.
641
+ - `parent` (`string | null`): The value of the span it's nested in, or `null` at the top level.
642
+ - `level` (`number`): Its depth, 1 at the top level.
643
+ - `index` (`number`): Its place among the rows that show, from 0.
644
+
645
+ ### State
646
+
647
+ What render functions, state-based class names and slots receive, per part.
648
+
649
+ `Waterfall`:
650
+
651
+ - `range` (`[number, number]`): The stretch of time the axis shows, as `[start, end]` in milliseconds.
652
+
653
+ `WaterfallAxis`:
654
+
655
+ - `ticks` (`WaterfallTickData[]`): The ticks it shows, at round times inside the range.
656
+
657
+ `WaterfallTick` and `WaterfallMark`:
658
+
659
+ - `time` (`number`): The moment it marks, in milliseconds.
660
+ - `label` (`string`): The time as `format` and the locale show it.
661
+ - `position` (`number`): Where it sits between the range's start and end, from 0 to 100.
662
+ - `outside` (`boolean`): Whether its time is outside the range, so it doesn't show on the axis.
663
+
664
+ `WaterfallRow`, and its `WaterfallRowHeader`, `WaterfallIndicator`, `WaterfallCell` and `WaterfallBar`:
665
+
666
+ - `expanded` (`boolean`): Whether it's a span with children that's expanded.
667
+ - `selected` (`boolean`): Whether it's the selected span.
668
+ - `branch` (`boolean`): Whether its span has children.
669
+ - `level` (`number`): Its depth, 1 at the top level.
670
+
671
+ ### Reasons
672
+
673
+ Why spans expanded or collapsed:
674
+
675
+ - `trigger-press` (`MouseEvent | KeyboardEvent | TouchEvent`): Click, tap, Enter or Space on the part that opens, closes or toggles. Arrow keys that open a menu or submenu count too. A tap on a combobox's input that opens a sheet reports its `touchend`.
676
+ - `list-navigation` (`KeyboardEvent`): Arrow, Home or End keys moved the selection, typing picked an item, or Ctrl or ⌘ with A selected every item of a listbox.
677
+ - `imperative` (`none`): A controller or `api` call, or a property set on a web component.
678
+
679
+ Why the selection changed:
680
+
681
+ - `imperative` (`none`): A controller or `api` call, or a property set on a web component.
682
+ - `item-press` (`MouseEvent | KeyboardEvent`): A member of a collection was chosen: a menu item, a tab, a radio or a checkbox item.
683
+
684
+ ### Controller
685
+
686
+ Create one with `createWaterfallController()` and pass it as `controller`.
687
+
688
+ - `getValue()` (`() => string | null`): The selected span's value, or `null`.
689
+ - `setValue(value, options?)` (`(value: string | null, options?: WaterfallChangeOptions<WaterfallValueChangeReason> | undefined) => boolean`): Selects a span, or nothing with `null`. Returns false when canceled.
690
+ - `getExpanded()` (`() => string[]`): The values of the expanded spans.
691
+ - `setExpanded(expanded, options?)` (`(expanded: string[], options?: WaterfallChangeOptions<WaterfallExpandedChangeReason> | undefined) => boolean`): Expands exactly these spans. Returns false when canceled.
692
+ - `isExpanded(value)` (`(value: string) => boolean`): Whether a span is expanded.
693
+ - `expand(value, options?)` (`(value: string, options?: WaterfallChangeOptions<WaterfallExpandedChangeReason> | undefined) => boolean`): Expands a span.
694
+ - `collapse(value, options?)` (`(value: string, options?: WaterfallChangeOptions<WaterfallExpandedChangeReason> | undefined) => boolean`): Collapses a span. Focus inside it moves to its row.
695
+ - `toggle(value, options?)` (`(value: string, options?: WaterfallChangeOptions<WaterfallExpandedChangeReason> | undefined) => boolean`): Expands a collapsed span, or collapses an expanded one.
696
+ - `expandAll(options?)` (`(options?: WaterfallChangeOptions<WaterfallExpandedChangeReason> | undefined) => boolean`): Expands every span with children.
697
+ - `collapseAll(options?)` (`(options?: WaterfallChangeOptions<WaterfallExpandedChangeReason> | undefined) => boolean`): Collapses every span.
698
+ - `reveal(value)` (`(value: string) => void`): Expands a span's ancestors and focuses its row, through `onHighlight` when it isn't rendered.
699
+ - `focus(value?)` (`(value?: string | undefined) => void`): Focuses a span's row, or the row with the tab stop.
700
+
701
+ ## Accessibility
702
+
703
+ The waterfall follows the [WAI-ARIA treegrid pattern](https://www.w3.org/WAI/ARIA/apg/patterns/treegrid/). Rows have the `row` role with `aria-level`, `aria-posinset`, `aria-setsize`, `aria-selected="true"` on the selected row, and `aria-expanded` on spans with children. `WaterfallRowHeader` is a `rowheader`, so screen readers repeat the span's name as focus moves down a column, and each `WaterfallBar` is a `gridcell` labeled with when its span starts and how long it lasts, like "Starts at 120 ms, lasts 340 ms". Times are formatted with `Intl` in the page's language.
704
+
705
+ ### Keyboard
706
+
707
+ The waterfall has one tab stop. Tab enters at the selected row, or the first row, then returns to the row or cell focused last.
708
+
709
+ | Key | Action |
710
+ | -------------------- | --------------------------------------------------------------------------------------- |
711
+ | Down / Up Arrow | Focus the next or previous row. In a cell, keep the column. |
712
+ | Right Arrow | On a row, expand it, or move into its first cell. In a cell, move right. |
713
+ | Left Arrow | In a cell, move left, then back to the row. On a row, collapse it, or focus its parent. |
714
+ | Home / End | Focus the first or last row. In a cell, the row's first or last cell. |
715
+ | Ctrl+Home / Ctrl+End | Focus the first or last row, keeping the column. |
716
+ | Page Up / Page Down | Move a page of rows. |
717
+ | Enter / Space | Select the row. |
718
+ | Typing | Focus the next row whose label starts with that text. |
719
+
720
+ Command replaces Ctrl on a Mac. In right-to-left text, Left and Right swap.
721
+
722
+ ### Tested Behavior
723
+
724
+ Every line below is an automated test.
725
+
726
+ **Rows**
727
+
728
+ - Spans start collapsed, and only top-level rows show.
729
+ - Clicking the indicator expands a span without selecting it.
730
+ - Clicking a row's cell selects the row and focuses the cell.
731
+ - A change can be canceled.
732
+ - A controller expands and collapses everything, and reveals a span.
733
+ - Collapsing a span from its indicator moves focus out of it.
734
+
735
+ **Keyboard**
736
+
737
+ - The waterfall is one tab stop.
738
+ - Down and Up move between rows, Home and End to the first and last.
739
+ - Right expands a collapsed row, then moves into its cells.
740
+ - Left moves back through the cells to the row, collapses it, then moves to the parent.
741
+ - Down and Up in a cell keep its column.
742
+ - Home and End in a cell go to the row's ends, and with Control to the column's.
743
+ - A focused cell keeps the tab stop.
744
+ - A cell hidden by a collapse gives up the tab stop.
745
+ - Enter and Space select the focused row.
746
+ - Typing moves to the next row whose name starts with it.
747
+
748
+ **Time axis**
749
+
750
+ - Each row places its bar as a share of the range.
751
+ - Each bar tells screen readers when its span starts and how long it lasts.
752
+ - The axis shows round times, hidden from screen readers.
753
+
754
+ Runs in React, Vue and web components.