@wallarm-org/claude-design 0.0.0-stage → 4.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (963) hide show
  1. package/README.md +114 -2
  2. package/bin/claude-design.js +347 -0
  3. package/dist/SETUP.md +130 -0
  4. package/dist/UPDATE.md +84 -0
  5. package/dist/assets/Icons/activity.svg +1 -0
  6. package/dist/assets/Icons/alt.svg +1 -0
  7. package/dist/assets/Icons/android-neutral.svg +1 -0
  8. package/dist/assets/Icons/android.svg +1 -0
  9. package/dist/assets/Icons/api.svg +1 -0
  10. package/dist/assets/Icons/apple-music-neutral.svg +1 -0
  11. package/dist/assets/Icons/apple-music.svg +1 -0
  12. package/dist/assets/Icons/apple-neutral.svg +1 -0
  13. package/dist/assets/Icons/apple-podcasts-neutral.svg +1 -0
  14. package/dist/assets/Icons/apple-podcasts.svg +1 -0
  15. package/dist/assets/Icons/apple.svg +1 -0
  16. package/dist/assets/Icons/archive.svg +1 -0
  17. package/dist/assets/Icons/arrow-big-up-dash.svg +1 -0
  18. package/dist/assets/Icons/arrow-big-up.svg +1 -0
  19. package/dist/assets/Icons/arrow-down-left.svg +1 -0
  20. package/dist/assets/Icons/arrow-down-right.svg +1 -0
  21. package/dist/assets/Icons/arrow-down.svg +1 -0
  22. package/dist/assets/Icons/arrow-left.svg +1 -0
  23. package/dist/assets/Icons/arrow-right-alt.svg +1 -0
  24. package/dist/assets/Icons/arrow-right.svg +1 -0
  25. package/dist/assets/Icons/arrow-up-down.svg +1 -0
  26. package/dist/assets/Icons/arrow-up-left.svg +1 -0
  27. package/dist/assets/Icons/arrow-up-right.svg +1 -0
  28. package/dist/assets/Icons/arrow-up.svg +1 -0
  29. package/dist/assets/Icons/artstation-neutral.svg +1 -0
  30. package/dist/assets/Icons/artstation.svg +1 -0
  31. package/dist/assets/Icons/asterisk.svg +1 -0
  32. package/dist/assets/Icons/audio-waveform.svg +1 -0
  33. package/dist/assets/Icons/aws-neutral.svg +1 -0
  34. package/dist/assets/Icons/aws.svg +1 -0
  35. package/dist/assets/Icons/badge-check.svg +1 -0
  36. package/dist/assets/Icons/baidu-neutral.svg +1 -0
  37. package/dist/assets/Icons/baidu.svg +1 -0
  38. package/dist/assets/Icons/bandage.svg +1 -0
  39. package/dist/assets/Icons/behance-neutral.svg +1 -0
  40. package/dist/assets/Icons/behance.svg +1 -0
  41. package/dist/assets/Icons/bell-cog.svg +1 -0
  42. package/dist/assets/Icons/bell.svg +1 -0
  43. package/dist/assets/Icons/binary.svg +1 -0
  44. package/dist/assets/Icons/book-open.svg +1 -0
  45. package/dist/assets/Icons/book.svg +1 -0
  46. package/dist/assets/Icons/boosty-neutral.svg +1 -0
  47. package/dist/assets/Icons/boosty.svg +1 -0
  48. package/dist/assets/Icons/bot.svg +1 -0
  49. package/dist/assets/Icons/brain.svg +1 -0
  50. package/dist/assets/Icons/braze-neutral.svg +1 -0
  51. package/dist/assets/Icons/braze.svg +1 -0
  52. package/dist/assets/Icons/brevo-neutral.svg +1 -0
  53. package/dist/assets/Icons/brevo.svg +1 -0
  54. package/dist/assets/Icons/bug-off.svg +1 -0
  55. package/dist/assets/Icons/bug-play.svg +1 -0
  56. package/dist/assets/Icons/bug.svg +1 -0
  57. package/dist/assets/Icons/building.svg +1 -0
  58. package/dist/assets/Icons/building2.svg +1 -0
  59. package/dist/assets/Icons/calendar-check.svg +1 -0
  60. package/dist/assets/Icons/calendar-check2.svg +1 -0
  61. package/dist/assets/Icons/calendar-clock.svg +1 -0
  62. package/dist/assets/Icons/calendar-cog.svg +1 -0
  63. package/dist/assets/Icons/calendar-days.svg +1 -0
  64. package/dist/assets/Icons/calendar-fold.svg +1 -0
  65. package/dist/assets/Icons/calendar-heart.svg +1 -0
  66. package/dist/assets/Icons/calendar-minus.svg +1 -0
  67. package/dist/assets/Icons/calendar-minus2.svg +1 -0
  68. package/dist/assets/Icons/calendar-off.svg +1 -0
  69. package/dist/assets/Icons/calendar-plus.svg +1 -0
  70. package/dist/assets/Icons/calendar-plus2.svg +1 -0
  71. package/dist/assets/Icons/calendar-range.svg +1 -0
  72. package/dist/assets/Icons/calendar-search.svg +1 -0
  73. package/dist/assets/Icons/calendar-x.svg +1 -0
  74. package/dist/assets/Icons/calendar-x2.svg +1 -0
  75. package/dist/assets/Icons/calendar.svg +1 -0
  76. package/dist/assets/Icons/chart-area.svg +1 -0
  77. package/dist/assets/Icons/chart-bar.svg +1 -0
  78. package/dist/assets/Icons/chart-column.svg +1 -0
  79. package/dist/assets/Icons/chart-no-axes-column.svg +1 -0
  80. package/dist/assets/Icons/check-check.svg +1 -0
  81. package/dist/assets/Icons/check.svg +1 -0
  82. package/dist/assets/Icons/checkbox-indeterminate.svg +1 -0
  83. package/dist/assets/Icons/chevron-down.svg +1 -0
  84. package/dist/assets/Icons/chevron-first.svg +1 -0
  85. package/dist/assets/Icons/chevron-last.svg +1 -0
  86. package/dist/assets/Icons/chevron-left.svg +1 -0
  87. package/dist/assets/Icons/chevron-right.svg +1 -0
  88. package/dist/assets/Icons/chevron-up-down.svg +1 -0
  89. package/dist/assets/Icons/chevron-up.svg +1 -0
  90. package/dist/assets/Icons/chevrons-down-up.svg +1 -0
  91. package/dist/assets/Icons/chevrons-down.svg +1 -0
  92. package/dist/assets/Icons/chevrons-left-right.svg +1 -0
  93. package/dist/assets/Icons/chevrons-left.svg +1 -0
  94. package/dist/assets/Icons/chevrons-right-left.svg +1 -0
  95. package/dist/assets/Icons/chevrons-right.svg +1 -0
  96. package/dist/assets/Icons/chevrons-up-down.svg +1 -0
  97. package/dist/assets/Icons/chevrons-up.svg +1 -0
  98. package/dist/assets/Icons/circle-arrow-down.svg +1 -0
  99. package/dist/assets/Icons/circle-arrow-left.svg +1 -0
  100. package/dist/assets/Icons/circle-arrow-out-down-left.svg +1 -0
  101. package/dist/assets/Icons/circle-arrow-out-down-right.svg +1 -0
  102. package/dist/assets/Icons/circle-arrow-out-up-left.svg +1 -0
  103. package/dist/assets/Icons/circle-arrow-out-up-right.svg +1 -0
  104. package/dist/assets/Icons/circle-arrow-right.svg +1 -0
  105. package/dist/assets/Icons/circle-arrow-up.svg +1 -0
  106. package/dist/assets/Icons/circle-check-big.svg +1 -0
  107. package/dist/assets/Icons/circle-check.svg +1 -0
  108. package/dist/assets/Icons/circle-chevron-down.svg +1 -0
  109. package/dist/assets/Icons/circle-chevron-left.svg +1 -0
  110. package/dist/assets/Icons/circle-chevron-right.svg +1 -0
  111. package/dist/assets/Icons/circle-chevron-up.svg +1 -0
  112. package/dist/assets/Icons/circle-dashed.svg +1 -0
  113. package/dist/assets/Icons/circle-dot-dashed.svg +1 -0
  114. package/dist/assets/Icons/circle-ellipsis.svg +1 -0
  115. package/dist/assets/Icons/circle-gauge.svg +1 -0
  116. package/dist/assets/Icons/circle-help.svg +1 -0
  117. package/dist/assets/Icons/circle-minus.svg +1 -0
  118. package/dist/assets/Icons/circle-plus.svg +1 -0
  119. package/dist/assets/Icons/circle-slash.svg +1 -0
  120. package/dist/assets/Icons/circle-slash2.svg +1 -0
  121. package/dist/assets/Icons/circle-stop.svg +1 -0
  122. package/dist/assets/Icons/circle.svg +1 -0
  123. package/dist/assets/Icons/clipboard-check.svg +1 -0
  124. package/dist/assets/Icons/clipboard-x.svg +1 -0
  125. package/dist/assets/Icons/clipboard.svg +1 -0
  126. package/dist/assets/Icons/clock-fading.svg +1 -0
  127. package/dist/assets/Icons/clock.svg +1 -0
  128. package/dist/assets/Icons/cloud-off.svg +1 -0
  129. package/dist/assets/Icons/cloud-sync.svg +1 -0
  130. package/dist/assets/Icons/cloud.svg +1 -0
  131. package/dist/assets/Icons/code.svg +1 -0
  132. package/dist/assets/Icons/collapse.svg +1 -0
  133. package/dist/assets/Icons/columns3-cog.svg +1 -0
  134. package/dist/assets/Icons/command.svg +1 -0
  135. package/dist/assets/Icons/compass.svg +1 -0
  136. package/dist/assets/Icons/copy-slash.svg +1 -0
  137. package/dist/assets/Icons/copy.svg +1 -0
  138. package/dist/assets/Icons/corner-down-left.svg +1 -0
  139. package/dist/assets/Icons/corner-down-right.svg +1 -0
  140. package/dist/assets/Icons/corner-left-down.svg +1 -0
  141. package/dist/assets/Icons/corner-left-up.svg +1 -0
  142. package/dist/assets/Icons/corner-right-down.svg +1 -0
  143. package/dist/assets/Icons/corner-right-up.svg +1 -0
  144. package/dist/assets/Icons/corner-up-left.svg +1 -0
  145. package/dist/assets/Icons/corner-up-right.svg +1 -0
  146. package/dist/assets/Icons/cpu.svg +1 -0
  147. package/dist/assets/Icons/credit-card.svg +1 -0
  148. package/dist/assets/Icons/crosshair.svg +1 -0
  149. package/dist/assets/Icons/ctrl.svg +1 -0
  150. package/dist/assets/Icons/database.svg +1 -0
  151. package/dist/assets/Icons/devianart-neutral.svg +1 -0
  152. package/dist/assets/Icons/devianart.svg +1 -0
  153. package/dist/assets/Icons/discord-neutral.svg +1 -0
  154. package/dist/assets/Icons/discord.svg +1 -0
  155. package/dist/assets/Icons/dollar-sign.svg +1 -0
  156. package/dist/assets/Icons/dot.svg +1 -0
  157. package/dist/assets/Icons/download.svg +1 -0
  158. package/dist/assets/Icons/dprofile-neutral.svg +1 -0
  159. package/dist/assets/Icons/dprofile.svg +1 -0
  160. package/dist/assets/Icons/dribbble-neutral.svg +1 -0
  161. package/dist/assets/Icons/dribbble.svg +1 -0
  162. package/dist/assets/Icons/droplet.svg +1 -0
  163. package/dist/assets/Icons/duplicate.svg +1 -0
  164. package/dist/assets/Icons/dzen-neutral.svg +1 -0
  165. package/dist/assets/Icons/dzen.svg +1 -0
  166. package/dist/assets/Icons/earth-lock.svg +1 -0
  167. package/dist/assets/Icons/earth.svg +1 -0
  168. package/dist/assets/Icons/ellipsis.svg +1 -0
  169. package/dist/assets/Icons/expand.svg +1 -0
  170. package/dist/assets/Icons/external-link.svg +1 -0
  171. package/dist/assets/Icons/eye-off.svg +1 -0
  172. package/dist/assets/Icons/eye.svg +1 -0
  173. package/dist/assets/Icons/facebook-neutral.svg +1 -0
  174. package/dist/assets/Icons/facebook.svg +1 -0
  175. package/dist/assets/Icons/fast-forward.svg +1 -0
  176. package/dist/assets/Icons/figma-neutral.svg +1 -0
  177. package/dist/assets/Icons/figma.svg +1 -0
  178. package/dist/assets/Icons/file-down.svg +1 -0
  179. package/dist/assets/Icons/file-plus.svg +1 -0
  180. package/dist/assets/Icons/file-text.svg +1 -0
  181. package/dist/assets/Icons/file.svg +1 -0
  182. package/dist/assets/Icons/film.svg +1 -0
  183. package/dist/assets/Icons/filter-x.svg +1 -0
  184. package/dist/assets/Icons/filter.svg +1 -0
  185. package/dist/assets/Icons/fingerprint-pattern.svg +1 -0
  186. package/dist/assets/Icons/flag.svg +1 -0
  187. package/dist/assets/Icons/flask-conical.svg +1 -0
  188. package/dist/assets/Icons/folder.svg +1 -0
  189. package/dist/assets/Icons/funnel-plus.svg +1 -0
  190. package/dist/assets/Icons/ghost.svg +1 -0
  191. package/dist/assets/Icons/git-branch.svg +1 -0
  192. package/dist/assets/Icons/git-commit-horizontal.svg +1 -0
  193. package/dist/assets/Icons/git-commit-vertical.svg +1 -0
  194. package/dist/assets/Icons/git-compare-arrows-alt.svg +1 -0
  195. package/dist/assets/Icons/git-compare-arrows.svg +1 -0
  196. package/dist/assets/Icons/git-compare.svg +1 -0
  197. package/dist/assets/Icons/git-fork.svg +1 -0
  198. package/dist/assets/Icons/git-graph.svg +1 -0
  199. package/dist/assets/Icons/git-merge.svg +1 -0
  200. package/dist/assets/Icons/git-pull-request-arrow.svg +1 -0
  201. package/dist/assets/Icons/git-pull-request-closed.svg +1 -0
  202. package/dist/assets/Icons/git-pull-request-create-arrow.svg +1 -0
  203. package/dist/assets/Icons/git-pull-request-create.svg +1 -0
  204. package/dist/assets/Icons/git-pull-request-draft.svg +1 -0
  205. package/dist/assets/Icons/git-pull-request.svg +1 -0
  206. package/dist/assets/Icons/github-neutral.svg +1 -0
  207. package/dist/assets/Icons/github.svg +1 -0
  208. package/dist/assets/Icons/globe-lock.svg +1 -0
  209. package/dist/assets/Icons/globe.svg +1 -0
  210. package/dist/assets/Icons/gmail-neutral.svg +1 -0
  211. package/dist/assets/Icons/gmail.svg +1 -0
  212. package/dist/assets/Icons/google-meet-neutral.svg +1 -0
  213. package/dist/assets/Icons/google-meet.svg +1 -0
  214. package/dist/assets/Icons/google-neutral.svg +1 -0
  215. package/dist/assets/Icons/google-play-neutral.svg +1 -0
  216. package/dist/assets/Icons/google-play.svg +1 -0
  217. package/dist/assets/Icons/google-podcast-neutral.svg +1 -0
  218. package/dist/assets/Icons/google-podcast.svg +1 -0
  219. package/dist/assets/Icons/google.svg +1 -0
  220. package/dist/assets/Icons/grip-horizontal.svg +1 -0
  221. package/dist/assets/Icons/grip-vertical.svg +1 -0
  222. package/dist/assets/Icons/hammer.svg +1 -0
  223. package/dist/assets/Icons/heart-pulse.svg +1 -0
  224. package/dist/assets/Icons/history.svg +1 -0
  225. package/dist/assets/Icons/home.svg +1 -0
  226. package/dist/assets/Icons/imo-neutral.svg +1 -0
  227. package/dist/assets/Icons/imo.svg +1 -0
  228. package/dist/assets/Icons/infinity.svg +1 -0
  229. package/dist/assets/Icons/info.svg +1 -0
  230. package/dist/assets/Icons/infobip-neutral.svg +1 -0
  231. package/dist/assets/Icons/infobip.svg +1 -0
  232. package/dist/assets/Icons/instagram-neutral.svg +1 -0
  233. package/dist/assets/Icons/instagram.svg +1 -0
  234. package/dist/assets/Icons/key-round.svg +1 -0
  235. package/dist/assets/Icons/key-square.svg +1 -0
  236. package/dist/assets/Icons/keyboard.svg +1 -0
  237. package/dist/assets/Icons/kickstarter-neutral.svg +1 -0
  238. package/dist/assets/Icons/kickstarter.svg +1 -0
  239. package/dist/assets/Icons/laptop.svg +1 -0
  240. package/dist/assets/Icons/layers.svg +1 -0
  241. package/dist/assets/Icons/layers2.svg +1 -0
  242. package/dist/assets/Icons/layers3.svg +1 -0
  243. package/dist/assets/Icons/layout-dashboard.svg +1 -0
  244. package/dist/assets/Icons/layout-grid.svg +1 -0
  245. package/dist/assets/Icons/layout-list.svg +1 -0
  246. package/dist/assets/Icons/layout-panel-left.svg +1 -0
  247. package/dist/assets/Icons/layout-panel-top.svg +1 -0
  248. package/dist/assets/Icons/layout-template.svg +1 -0
  249. package/dist/assets/Icons/library-big.svg +1 -0
  250. package/dist/assets/Icons/life-buoy.svg +1 -0
  251. package/dist/assets/Icons/lightbulb-off.svg +1 -0
  252. package/dist/assets/Icons/lightbulb.svg +1 -0
  253. package/dist/assets/Icons/line-neutral.svg +1 -0
  254. package/dist/assets/Icons/line.svg +1 -0
  255. package/dist/assets/Icons/link.svg +1 -0
  256. package/dist/assets/Icons/link2-off.svg +1 -0
  257. package/dist/assets/Icons/link2.svg +1 -0
  258. package/dist/assets/Icons/linkedin-neutral.svg +1 -0
  259. package/dist/assets/Icons/linkedin.svg +1 -0
  260. package/dist/assets/Icons/list-checks.svg +1 -0
  261. package/dist/assets/Icons/list-collapse.svg +1 -0
  262. package/dist/assets/Icons/list-filter.svg +1 -0
  263. package/dist/assets/Icons/list-minus.svg +1 -0
  264. package/dist/assets/Icons/list-ordered.svg +1 -0
  265. package/dist/assets/Icons/list-plus.svg +1 -0
  266. package/dist/assets/Icons/list-restart.svg +1 -0
  267. package/dist/assets/Icons/list-start.svg +1 -0
  268. package/dist/assets/Icons/list-todo.svg +1 -0
  269. package/dist/assets/Icons/list-tree.svg +1 -0
  270. package/dist/assets/Icons/list-video.svg +1 -0
  271. package/dist/assets/Icons/list-x.svg +1 -0
  272. package/dist/assets/Icons/list.svg +1 -0
  273. package/dist/assets/Icons/loader-circle.svg +1 -0
  274. package/dist/assets/Icons/loader.svg +1 -0
  275. package/dist/assets/Icons/lock-open.svg +1 -0
  276. package/dist/assets/Icons/lock.svg +1 -0
  277. package/dist/assets/Icons/log-in.svg +1 -0
  278. package/dist/assets/Icons/log-out.svg +1 -0
  279. package/dist/assets/Icons/mail-check.svg +1 -0
  280. package/dist/assets/Icons/mail-minus.svg +1 -0
  281. package/dist/assets/Icons/mail-open.svg +1 -0
  282. package/dist/assets/Icons/mail-plus.svg +1 -0
  283. package/dist/assets/Icons/mail-question.svg +1 -0
  284. package/dist/assets/Icons/mail-search.svg +1 -0
  285. package/dist/assets/Icons/mail-warning.svg +1 -0
  286. package/dist/assets/Icons/mail-x.svg +1 -0
  287. package/dist/assets/Icons/mail.svg +1 -0
  288. package/dist/assets/Icons/mailbox.svg +1 -0
  289. package/dist/assets/Icons/mailersend-neutral.svg +1 -0
  290. package/dist/assets/Icons/mailersend.svg +1 -0
  291. package/dist/assets/Icons/mailgun-neutral.svg +1 -0
  292. package/dist/assets/Icons/mailgun.svg +1 -0
  293. package/dist/assets/Icons/mailjet-neutral.svg +1 -0
  294. package/dist/assets/Icons/mailjet.svg +1 -0
  295. package/dist/assets/Icons/mails.svg +1 -0
  296. package/dist/assets/Icons/mailtrap-neutral.svg +1 -0
  297. package/dist/assets/Icons/mailtrap.svg +1 -0
  298. package/dist/assets/Icons/mandrill-neutral.svg +1 -0
  299. package/dist/assets/Icons/mandrill.svg +1 -0
  300. package/dist/assets/Icons/map-pin.svg +1 -0
  301. package/dist/assets/Icons/maqsam-neutral.svg +1 -0
  302. package/dist/assets/Icons/maqsam.svg +1 -0
  303. package/dist/assets/Icons/mattermost-neutral.svg +1 -0
  304. package/dist/assets/Icons/mattermost.svg +1 -0
  305. package/dist/assets/Icons/maximize.svg +1 -0
  306. package/dist/assets/Icons/maximize2.svg +1 -0
  307. package/dist/assets/Icons/medium-neutral.svg +1 -0
  308. package/dist/assets/Icons/medium.svg +1 -0
  309. package/dist/assets/Icons/megaphone-off.svg +1 -0
  310. package/dist/assets/Icons/megaphone.svg +1 -0
  311. package/dist/assets/Icons/menu.svg +1 -0
  312. package/dist/assets/Icons/merge.svg +1 -0
  313. package/dist/assets/Icons/message-square-code.svg +1 -0
  314. package/dist/assets/Icons/message-square-dashed.svg +1 -0
  315. package/dist/assets/Icons/message-square-diff.svg +1 -0
  316. package/dist/assets/Icons/message-square-dot.svg +1 -0
  317. package/dist/assets/Icons/message-square-heart.svg +1 -0
  318. package/dist/assets/Icons/message-square-off.svg +1 -0
  319. package/dist/assets/Icons/message-square-plus.svg +1 -0
  320. package/dist/assets/Icons/message-square-quote.svg +1 -0
  321. package/dist/assets/Icons/message-square-reply.svg +1 -0
  322. package/dist/assets/Icons/message-square-share.svg +1 -0
  323. package/dist/assets/Icons/message-square-text.svg +1 -0
  324. package/dist/assets/Icons/message-square-warning.svg +1 -0
  325. package/dist/assets/Icons/message-square-x.svg +1 -0
  326. package/dist/assets/Icons/message-square.svg +1 -0
  327. package/dist/assets/Icons/messages-square.svg +1 -0
  328. package/dist/assets/Icons/messenger-neutral.svg +1 -0
  329. package/dist/assets/Icons/messenger.svg +1 -0
  330. package/dist/assets/Icons/microsoft-neutral.svg +1 -0
  331. package/dist/assets/Icons/microsoft-teams-neutral.svg +1 -0
  332. package/dist/assets/Icons/microsoft-teams.svg +1 -0
  333. package/dist/assets/Icons/microsoft.svg +1 -0
  334. package/dist/assets/Icons/minimize.svg +1 -0
  335. package/dist/assets/Icons/minimize2.svg +1 -0
  336. package/dist/assets/Icons/minus.svg +1 -0
  337. package/dist/assets/Icons/moon.svg +1 -0
  338. package/dist/assets/Icons/mouse-pointer-ban.svg +1 -0
  339. package/dist/assets/Icons/mouse-pointer-click.svg +1 -0
  340. package/dist/assets/Icons/mouse-pointer.svg +1 -0
  341. package/dist/assets/Icons/mouse-pointer2.svg +1 -0
  342. package/dist/assets/Icons/mouse.svg +1 -0
  343. package/dist/assets/Icons/move-diagonal.svg +1 -0
  344. package/dist/assets/Icons/move-diagonal2.svg +1 -0
  345. package/dist/assets/Icons/move-down-left.svg +1 -0
  346. package/dist/assets/Icons/move-down-right.svg +1 -0
  347. package/dist/assets/Icons/move-down.svg +1 -0
  348. package/dist/assets/Icons/move-horizontal.svg +1 -0
  349. package/dist/assets/Icons/move-left.svg +1 -0
  350. package/dist/assets/Icons/move-right.svg +1 -0
  351. package/dist/assets/Icons/move-up-left.svg +1 -0
  352. package/dist/assets/Icons/move-up-right.svg +1 -0
  353. package/dist/assets/Icons/move-up.svg +1 -0
  354. package/dist/assets/Icons/move-vertical.svg +1 -0
  355. package/dist/assets/Icons/move3-d.svg +1 -0
  356. package/dist/assets/Icons/netcore-neutral.svg +1 -0
  357. package/dist/assets/Icons/netcore.svg +1 -0
  358. package/dist/assets/Icons/notebook-pen.svg +1 -0
  359. package/dist/assets/Icons/notebook-tabs.svg +1 -0
  360. package/dist/assets/Icons/notebook-text.svg +1 -0
  361. package/dist/assets/Icons/notebook.svg +1 -0
  362. package/dist/assets/Icons/notepad-text-dashed.svg +1 -0
  363. package/dist/assets/Icons/notepad-text.svg +1 -0
  364. package/dist/assets/Icons/notion-neutral.svg +1 -0
  365. package/dist/assets/Icons/notion.svg +1 -0
  366. package/dist/assets/Icons/octagon-alert.svg +1 -0
  367. package/dist/assets/Icons/octagon-pause.svg +1 -0
  368. package/dist/assets/Icons/octagon-x.svg +1 -0
  369. package/dist/assets/Icons/octagon.svg +1 -0
  370. package/dist/assets/Icons/ok-neutral.svg +1 -0
  371. package/dist/assets/Icons/ok-only-sign-neutral.svg +1 -0
  372. package/dist/assets/Icons/ok-only-sign.svg +1 -0
  373. package/dist/assets/Icons/ok.svg +1 -0
  374. package/dist/assets/Icons/onlyfans-neutral.svg +1 -0
  375. package/dist/assets/Icons/onlyfans.svg +1 -0
  376. package/dist/assets/Icons/option.svg +1 -0
  377. package/dist/assets/Icons/outlook365-neutral.svg +1 -0
  378. package/dist/assets/Icons/outlook365.svg +1 -0
  379. package/dist/assets/Icons/paintbrush.svg +1 -0
  380. package/dist/assets/Icons/panel-bottom-open.svg +1 -0
  381. package/dist/assets/Icons/panel-bottom.svg +1 -0
  382. package/dist/assets/Icons/panel-left-close.svg +1 -0
  383. package/dist/assets/Icons/panel-left-dashed.svg +1 -0
  384. package/dist/assets/Icons/panel-left-open.svg +1 -0
  385. package/dist/assets/Icons/panel-left.svg +1 -0
  386. package/dist/assets/Icons/panel-right-animated.svg +1 -0
  387. package/dist/assets/Icons/panel-right-close.svg +1 -0
  388. package/dist/assets/Icons/panel-right-dashed.svg +1 -0
  389. package/dist/assets/Icons/panel-right-open.svg +1 -0
  390. package/dist/assets/Icons/panel-right.svg +1 -0
  391. package/dist/assets/Icons/panel-top-bottom-dashed.svg +1 -0
  392. package/dist/assets/Icons/panel-top-close.svg +1 -0
  393. package/dist/assets/Icons/panel-top-open.svg +1 -0
  394. package/dist/assets/Icons/panel-top.svg +1 -0
  395. package/dist/assets/Icons/panels-left-bottom.svg +1 -0
  396. package/dist/assets/Icons/panels-right-bottom.svg +1 -0
  397. package/dist/assets/Icons/panels-top-left.svg +1 -0
  398. package/dist/assets/Icons/paperclip.svg +1 -0
  399. package/dist/assets/Icons/patreon-neutral.svg +1 -0
  400. package/dist/assets/Icons/patreon.svg +1 -0
  401. package/dist/assets/Icons/paypal-neutral.svg +1 -0
  402. package/dist/assets/Icons/paypal.svg +1 -0
  403. package/dist/assets/Icons/pen-line.svg +1 -0
  404. package/dist/assets/Icons/pen-off.svg +1 -0
  405. package/dist/assets/Icons/pen-tool.svg +1 -0
  406. package/dist/assets/Icons/pen.svg +1 -0
  407. package/dist/assets/Icons/pencil-line.svg +1 -0
  408. package/dist/assets/Icons/pencil-off.svg +1 -0
  409. package/dist/assets/Icons/pencil-ruler.svg +1 -0
  410. package/dist/assets/Icons/pencil.svg +1 -0
  411. package/dist/assets/Icons/pentagon.svg +1 -0
  412. package/dist/assets/Icons/percentage.svg +1 -0
  413. package/dist/assets/Icons/phone.svg +1 -0
  414. package/dist/assets/Icons/pin-off.svg +1 -0
  415. package/dist/assets/Icons/pin.svg +1 -0
  416. package/dist/assets/Icons/pinterest-neutral.svg +1 -0
  417. package/dist/assets/Icons/pinterest.svg +1 -0
  418. package/dist/assets/Icons/play.svg +1 -0
  419. package/dist/assets/Icons/plug.svg +1 -0
  420. package/dist/assets/Icons/plunk-neutral.svg +1 -0
  421. package/dist/assets/Icons/plunk.svg +1 -0
  422. package/dist/assets/Icons/plus.svg +1 -0
  423. package/dist/assets/Icons/postmark-neutral.svg +1 -0
  424. package/dist/assets/Icons/postmark.svg +1 -0
  425. package/dist/assets/Icons/product-hunt-neutral.svg +1 -0
  426. package/dist/assets/Icons/product-hunt.svg +1 -0
  427. package/dist/assets/Icons/quora-neutral.svg +1 -0
  428. package/dist/assets/Icons/quora.svg +1 -0
  429. package/dist/assets/Icons/quote.svg +1 -0
  430. package/dist/assets/Icons/radar.svg +1 -0
  431. package/dist/assets/Icons/reddit-neutral.svg +1 -0
  432. package/dist/assets/Icons/reddit.svg +1 -0
  433. package/dist/assets/Icons/redo-dot.svg +1 -0
  434. package/dist/assets/Icons/redo.svg +1 -0
  435. package/dist/assets/Icons/redo2.svg +1 -0
  436. package/dist/assets/Icons/refresh-ccw.svg +1 -0
  437. package/dist/assets/Icons/refresh-cw-off.svg +1 -0
  438. package/dist/assets/Icons/resend-neutral.svg +1 -0
  439. package/dist/assets/Icons/resend.svg +1 -0
  440. package/dist/assets/Icons/rss.svg +1 -0
  441. package/dist/assets/Icons/rules.svg +1 -0
  442. package/dist/assets/Icons/scan-line.svg +1 -0
  443. package/dist/assets/Icons/scroll-text.svg +1 -0
  444. package/dist/assets/Icons/search-hint.svg +1 -0
  445. package/dist/assets/Icons/search-x.svg +1 -0
  446. package/dist/assets/Icons/search.svg +1 -0
  447. package/dist/assets/Icons/sendgrid-neutral.svg +1 -0
  448. package/dist/assets/Icons/sendgrid.svg +1 -0
  449. package/dist/assets/Icons/server.svg +1 -0
  450. package/dist/assets/Icons/ses-neutral.svg +1 -0
  451. package/dist/assets/Icons/ses.svg +1 -0
  452. package/dist/assets/Icons/settings.svg +1 -0
  453. package/dist/assets/Icons/settings2.svg +1 -0
  454. package/dist/assets/Icons/shapes.svg +1 -0
  455. package/dist/assets/Icons/share.svg +1 -0
  456. package/dist/assets/Icons/sheet.svg +1 -0
  457. package/dist/assets/Icons/shield-alert.svg +1 -0
  458. package/dist/assets/Icons/shield-ban.svg +1 -0
  459. package/dist/assets/Icons/shield-check.svg +1 -0
  460. package/dist/assets/Icons/shield-ellipsis.svg +1 -0
  461. package/dist/assets/Icons/shield-half.svg +1 -0
  462. package/dist/assets/Icons/shield-minus.svg +1 -0
  463. package/dist/assets/Icons/shield-off.svg +1 -0
  464. package/dist/assets/Icons/shield-plus.svg +1 -0
  465. package/dist/assets/Icons/shield-question.svg +1 -0
  466. package/dist/assets/Icons/shield-user.svg +1 -0
  467. package/dist/assets/Icons/shield-x.svg +1 -0
  468. package/dist/assets/Icons/shield.svg +1 -0
  469. package/dist/assets/Icons/shift.svg +1 -0
  470. package/dist/assets/Icons/signal-neutral.svg +1 -0
  471. package/dist/assets/Icons/signal.svg +1 -0
  472. package/dist/assets/Icons/signpost.svg +1 -0
  473. package/dist/assets/Icons/sina-weibo-neutral.svg +1 -0
  474. package/dist/assets/Icons/sina-weibo.svg +1 -0
  475. package/dist/assets/Icons/skull.svg +1 -0
  476. package/dist/assets/Icons/skype-neutral.svg +1 -0
  477. package/dist/assets/Icons/skype.svg +1 -0
  478. package/dist/assets/Icons/slack-neutral.svg +1 -0
  479. package/dist/assets/Icons/slack.svg +1 -0
  480. package/dist/assets/Icons/sliders-horizontal.svg +1 -0
  481. package/dist/assets/Icons/sliders-vertical.svg +1 -0
  482. package/dist/assets/Icons/smtp-neutral.svg +1 -0
  483. package/dist/assets/Icons/smtp.svg +1 -0
  484. package/dist/assets/Icons/snapchat-neutral.svg +1 -0
  485. package/dist/assets/Icons/snapchat.svg +1 -0
  486. package/dist/assets/Icons/soundcloud-neutral.svg +1 -0
  487. package/dist/assets/Icons/soundcloud.svg +1 -0
  488. package/dist/assets/Icons/space.svg +1 -0
  489. package/dist/assets/Icons/sparkpost-neutral.svg +1 -0
  490. package/dist/assets/Icons/sparkpost.svg +1 -0
  491. package/dist/assets/Icons/speed.svg +1 -0
  492. package/dist/assets/Icons/split.svg +1 -0
  493. package/dist/assets/Icons/spotify-neutral.svg +1 -0
  494. package/dist/assets/Icons/spotify.svg +1 -0
  495. package/dist/assets/Icons/square-arrow-out-up-right.svg +1 -0
  496. package/dist/assets/Icons/square-minus.svg +1 -0
  497. package/dist/assets/Icons/square-pen.svg +1 -0
  498. package/dist/assets/Icons/square-plus.svg +1 -0
  499. package/dist/assets/Icons/square-user-round.svg +1 -0
  500. package/dist/assets/Icons/stack-overflow-neutral.svg +1 -0
  501. package/dist/assets/Icons/stack-overflow.svg +1 -0
  502. package/dist/assets/Icons/star.svg +1 -0
  503. package/dist/assets/Icons/sun.svg +1 -0
  504. package/dist/assets/Icons/syringe.svg +1 -0
  505. package/dist/assets/Icons/tag.svg +1 -0
  506. package/dist/assets/Icons/target.svg +1 -0
  507. package/dist/assets/Icons/teams-neutral.svg +1 -0
  508. package/dist/assets/Icons/teams.svg +1 -0
  509. package/dist/assets/Icons/telegram-neutral.svg +1 -0
  510. package/dist/assets/Icons/telegram-only-sign-neutral.svg +1 -0
  511. package/dist/assets/Icons/telegram-only-sign.svg +1 -0
  512. package/dist/assets/Icons/telegram.svg +1 -0
  513. package/dist/assets/Icons/terminal.svg +1 -0
  514. package/dist/assets/Icons/test-policies.svg +1 -0
  515. package/dist/assets/Icons/threads-neutral.svg +1 -0
  516. package/dist/assets/Icons/threads.svg +1 -0
  517. package/dist/assets/Icons/thumbs-down.svg +1 -0
  518. package/dist/assets/Icons/thumbs-up.svg +1 -0
  519. package/dist/assets/Icons/tiktok-neutral.svg +1 -0
  520. package/dist/assets/Icons/tiktok.svg +1 -0
  521. package/dist/assets/Icons/tinder-neutral.svg +1 -0
  522. package/dist/assets/Icons/tinder.svg +1 -0
  523. package/dist/assets/Icons/trash.svg +1 -0
  524. package/dist/assets/Icons/trash2.svg +1 -0
  525. package/dist/assets/Icons/triangle-alert.svg +1 -0
  526. package/dist/assets/Icons/tumblr-neutral.svg +1 -0
  527. package/dist/assets/Icons/tumblr.svg +1 -0
  528. package/dist/assets/Icons/twitch-neutral.svg +1 -0
  529. package/dist/assets/Icons/twitch.svg +1 -0
  530. package/dist/assets/Icons/undo-dot.svg +1 -0
  531. package/dist/assets/Icons/undo.svg +1 -0
  532. package/dist/assets/Icons/undo2.svg +1 -0
  533. package/dist/assets/Icons/unplug.svg +1 -0
  534. package/dist/assets/Icons/user-check.svg +1 -0
  535. package/dist/assets/Icons/user-minus.svg +1 -0
  536. package/dist/assets/Icons/user-plus.svg +1 -0
  537. package/dist/assets/Icons/user-round-x.svg +1 -0
  538. package/dist/assets/Icons/user-round.svg +1 -0
  539. package/dist/assets/Icons/user.svg +1 -0
  540. package/dist/assets/Icons/username.svg +1 -0
  541. package/dist/assets/Icons/users-round.svg +1 -0
  542. package/dist/assets/Icons/users.svg +1 -0
  543. package/dist/assets/Icons/viber-neutral.svg +1 -0
  544. package/dist/assets/Icons/viber.svg +1 -0
  545. package/dist/assets/Icons/video.svg +1 -0
  546. package/dist/assets/Icons/vimeo-neutral.svg +1 -0
  547. package/dist/assets/Icons/vimeo.svg +1 -0
  548. package/dist/assets/Icons/vk-music-neutral.svg +1 -0
  549. package/dist/assets/Icons/vk-music.svg +1 -0
  550. package/dist/assets/Icons/vk-neutral.svg +1 -0
  551. package/dist/assets/Icons/vk-only-sign-neutral.svg +1 -0
  552. package/dist/assets/Icons/vk-only-sign.svg +1 -0
  553. package/dist/assets/Icons/vk.svg +1 -0
  554. package/dist/assets/Icons/webhook-neutral.svg +1 -0
  555. package/dist/assets/Icons/webhook.svg +1 -0
  556. package/dist/assets/Icons/wechat-neutral.svg +1 -0
  557. package/dist/assets/Icons/wechat.svg +1 -0
  558. package/dist/assets/Icons/whatsapp-badge-neutral.svg +1 -0
  559. package/dist/assets/Icons/whatsapp-badge.svg +1 -0
  560. package/dist/assets/Icons/whatsapp-neutral.svg +1 -0
  561. package/dist/assets/Icons/whatsapp.svg +1 -0
  562. package/dist/assets/Icons/workflow.svg +1 -0
  563. package/dist/assets/Icons/wrap-text.svg +1 -0
  564. package/dist/assets/Icons/wrench.svg +1 -0
  565. package/dist/assets/Icons/x-ex-twitter-neutral.svg +1 -0
  566. package/dist/assets/Icons/x-ex-twitter.svg +1 -0
  567. package/dist/assets/Icons/x.svg +1 -0
  568. package/dist/assets/Icons/xing-neutral.svg +1 -0
  569. package/dist/assets/Icons/xing.svg +1 -0
  570. package/dist/assets/Icons/yandex-music-neutral.svg +1 -0
  571. package/dist/assets/Icons/yandex-music.svg +1 -0
  572. package/dist/assets/Icons/yelp-neutral.svg +1 -0
  573. package/dist/assets/Icons/yelp.svg +1 -0
  574. package/dist/assets/Icons/youtube-music-neutral.svg +1 -0
  575. package/dist/assets/Icons/youtube-music.svg +1 -0
  576. package/dist/assets/Icons/youtube-neutral.svg +1 -0
  577. package/dist/assets/Icons/youtube-shorts-neutral.svg +1 -0
  578. package/dist/assets/Icons/youtube-shorts.svg +1 -0
  579. package/dist/assets/Icons/youtube.svg +1 -0
  580. package/dist/assets/Icons/zap.svg +1 -0
  581. package/dist/assets/Icons/zoom-in.svg +1 -0
  582. package/dist/assets/Icons/zoom-neutral.svg +1 -0
  583. package/dist/assets/Icons/zoom-out.svg +1 -0
  584. package/dist/assets/Icons/zoom.svg +1 -0
  585. package/dist/assets/Icons/zulip-neutral.svg +1 -0
  586. package/dist/assets/Icons/zulip.svg +1 -0
  587. package/dist/assets/Logos/wallarm.svg +1 -0
  588. package/dist/assets.json +4666 -0
  589. package/dist/project/README.md +265 -0
  590. package/dist/project/components/Accordion/README.md +354 -0
  591. package/dist/project/components/Accordion/preview.html +2 -0
  592. package/dist/project/components/Accordion/stories.js +12 -0
  593. package/dist/project/components/AgentAvatar/README.md +127 -0
  594. package/dist/project/components/AgentAvatar/preview.html +2 -0
  595. package/dist/project/components/AgentAvatar/stories.js +12 -0
  596. package/dist/project/components/AgentAvatar/stories.md +1805 -0
  597. package/dist/project/components/AgentGlyph/README.md +1182 -0
  598. package/dist/project/components/AgentGlyph/preview.html +2 -0
  599. package/dist/project/components/AgentGlyph/stories.js +12 -0
  600. package/dist/project/components/AgentLookBuilder/README.md +269 -0
  601. package/dist/project/components/AgentLookBuilder/preview.html +2 -0
  602. package/dist/project/components/AgentLookBuilder/stories.js +12 -0
  603. package/dist/project/components/Alert/README.md +492 -0
  604. package/dist/project/components/Alert/preview.html +2 -0
  605. package/dist/project/components/Alert/stories.js +12 -0
  606. package/dist/project/components/AnimatedBackground/README.md +124 -0
  607. package/dist/project/components/AnimatedBackground/preview.html +2 -0
  608. package/dist/project/components/AnimatedBackground/stories.js +12 -0
  609. package/dist/project/components/AppShell/README.md +75 -0
  610. package/dist/project/components/AppShell/preview.html +2 -0
  611. package/dist/project/components/AppShell/stories.js +28 -0
  612. package/dist/project/components/AppShell/stories.md +2878 -0
  613. package/dist/project/components/ApprovalCard/README.md +353 -0
  614. package/dist/project/components/ApprovalCard/preview.html +2 -0
  615. package/dist/project/components/ApprovalCard/stories.js +12 -0
  616. package/dist/project/components/AreaChart/README.md +673 -0
  617. package/dist/project/components/AreaChart/preview.html +2 -0
  618. package/dist/project/components/AreaChart/stories.js +12 -0
  619. package/dist/project/components/AssistantMessage/README.md +724 -0
  620. package/dist/project/components/AssistantMessage/preview.html +2 -0
  621. package/dist/project/components/AssistantMessage/stories.js +55 -0
  622. package/dist/project/components/Attribute/README.md +1174 -0
  623. package/dist/project/components/Attribute/preview.html +2 -0
  624. package/dist/project/components/Attribute/stories.js +12 -0
  625. package/dist/project/components/Avatar/README.md +416 -0
  626. package/dist/project/components/Avatar/preview.html +2 -0
  627. package/dist/project/components/Avatar/stories.js +12 -0
  628. package/dist/project/components/Badge/README.md +853 -0
  629. package/dist/project/components/Badge/preview.html +2 -0
  630. package/dist/project/components/Badge/stories.js +12 -0
  631. package/dist/project/components/Banner/README.md +341 -0
  632. package/dist/project/components/Banner/preview.html +2 -0
  633. package/dist/project/components/Banner/stories.js +12 -0
  634. package/dist/project/components/BarList/README.md +757 -0
  635. package/dist/project/components/BarList/preview.html +2 -0
  636. package/dist/project/components/BarList/stories.js +12 -0
  637. package/dist/project/components/Breadcrumbs/README.md +274 -0
  638. package/dist/project/components/Breadcrumbs/preview.html +2 -0
  639. package/dist/project/components/Breadcrumbs/stories.js +12 -0
  640. package/dist/project/components/BulkBar/README.md +19 -0
  641. package/dist/project/components/Button/README.md +467 -0
  642. package/dist/project/components/Button/preview.html +2 -0
  643. package/dist/project/components/Button/stories.js +12 -0
  644. package/dist/project/components/Calendar/README.md +654 -0
  645. package/dist/project/components/Calendar/preview.html +2 -0
  646. package/dist/project/components/Calendar/stories.js +12 -0
  647. package/dist/project/components/Card/README.md +367 -0
  648. package/dist/project/components/Card/preview.html +2 -0
  649. package/dist/project/components/Card/stories.js +12 -0
  650. package/dist/project/components/ChainOfThought/README.md +412 -0
  651. package/dist/project/components/ChainOfThought/preview.html +2 -0
  652. package/dist/project/components/ChainOfThought/stories.js +12 -0
  653. package/dist/project/components/Chart/README.md +224 -0
  654. package/dist/project/components/Chart/preview.html +2 -0
  655. package/dist/project/components/Chart/stories.js +12 -0
  656. package/dist/project/components/Chat/README.md +1244 -0
  657. package/dist/project/components/Chat/preview.html +2 -0
  658. package/dist/project/components/Chat/stories.js +12 -0
  659. package/dist/project/components/ChatInput/README.md +378 -0
  660. package/dist/project/components/ChatInput/preview.html +2 -0
  661. package/dist/project/components/ChatInput/stories.js +12 -0
  662. package/dist/project/components/ChatPermissions/README.md +218 -0
  663. package/dist/project/components/ChatPermissions/preview.html +2 -0
  664. package/dist/project/components/ChatPermissions/stories.js +12 -0
  665. package/dist/project/components/Checkbox/README.md +367 -0
  666. package/dist/project/components/Checkbox/preview.html +2 -0
  667. package/dist/project/components/Checkbox/stories.js +12 -0
  668. package/dist/project/components/Code/README.md +243 -0
  669. package/dist/project/components/Code/preview.html +2 -0
  670. package/dist/project/components/Code/stories.js +12 -0
  671. package/dist/project/components/CodeEditor/README.md +280 -0
  672. package/dist/project/components/CodeEditor/preview.html +2 -0
  673. package/dist/project/components/CodeEditor/stories.js +162 -0
  674. package/dist/project/components/CodeEditor/stories.md +1533 -0
  675. package/dist/project/components/CodeSnippet/README.md +1352 -0
  676. package/dist/project/components/CodeSnippet/preview.html +2 -0
  677. package/dist/project/components/CodeSnippet/stories.js +156 -0
  678. package/dist/project/components/ColumnChart/README.md +521 -0
  679. package/dist/project/components/ColumnChart/preview.html +2 -0
  680. package/dist/project/components/ColumnChart/stories.js +12 -0
  681. package/dist/project/components/Conversation/README.md +314 -0
  682. package/dist/project/components/Conversation/preview.html +2 -0
  683. package/dist/project/components/Conversation/stories.js +12 -0
  684. package/dist/project/components/Copyable/README.md +406 -0
  685. package/dist/project/components/Copyable/preview.html +2 -0
  686. package/dist/project/components/Copyable/stories.js +12 -0
  687. package/dist/project/components/Country/README.md +226 -0
  688. package/dist/project/components/Country/preview.html +2 -0
  689. package/dist/project/components/Country/stories.js +12 -0
  690. package/dist/project/components/Cover/preview.html +43 -0
  691. package/dist/project/components/DateInput/README.md +458 -0
  692. package/dist/project/components/DateInput/preview.html +2 -0
  693. package/dist/project/components/DateInput/stories.js +12 -0
  694. package/dist/project/components/DateRangeInput/README.md +412 -0
  695. package/dist/project/components/DateRangeInput/preview.html +2 -0
  696. package/dist/project/components/DateRangeInput/stories.js +12 -0
  697. package/dist/project/components/Dialog/README.md +1140 -0
  698. package/dist/project/components/Dialog/preview.html +2 -0
  699. package/dist/project/components/Dialog/stories.js +12 -0
  700. package/dist/project/components/Drawer/README.md +1163 -0
  701. package/dist/project/components/Drawer/preview.html +2 -0
  702. package/dist/project/components/Drawer/stories.js +12 -0
  703. package/dist/project/components/DropdownMenu/README.md +678 -0
  704. package/dist/project/components/DropdownMenu/preview.html +2 -0
  705. package/dist/project/components/DropdownMenu/stories.js +12 -0
  706. package/dist/project/components/EmptyState/README.md +276 -0
  707. package/dist/project/components/EmptyState/preview.html +2 -0
  708. package/dist/project/components/EmptyState/stories.js +12 -0
  709. package/dist/project/components/FeedbackPulse/README.md +125 -0
  710. package/dist/project/components/FeedbackPulse/preview.html +2 -0
  711. package/dist/project/components/FeedbackPulse/stories.js +12 -0
  712. package/dist/project/components/Field/README.md +676 -0
  713. package/dist/project/components/Field/preview.html +2 -0
  714. package/dist/project/components/Field/stories.js +12 -0
  715. package/dist/project/components/FileUpload/README.md +544 -0
  716. package/dist/project/components/FileUpload/preview.html +2 -0
  717. package/dist/project/components/FileUpload/stories.js +12 -0
  718. package/dist/project/components/FilterCascade/README.md +348 -0
  719. package/dist/project/components/FilterCascade/preview.html +2 -0
  720. package/dist/project/components/FilterCascade/stories.js +12 -0
  721. package/dist/project/components/FilterDropdown/README.md +542 -0
  722. package/dist/project/components/FilterDropdown/preview.html +2 -0
  723. package/dist/project/components/FilterDropdown/stories.js +12 -0
  724. package/dist/project/components/FilterInput/README.md +416 -0
  725. package/dist/project/components/FilterInput/preview.html +2 -0
  726. package/dist/project/components/FilterInput/stories.js +12 -0
  727. package/dist/project/components/FilterInput/stories.md +6682 -0
  728. package/dist/project/components/Flex/README.md +225 -0
  729. package/dist/project/components/Flex/preview.html +2 -0
  730. package/dist/project/components/Flex/stories.js +12 -0
  731. package/dist/project/components/FormatDateTime/README.md +315 -0
  732. package/dist/project/components/FormatDateTime/preview.html +2 -0
  733. package/dist/project/components/FormatDateTime/stories.js +12 -0
  734. package/dist/project/components/FormatNumber/README.md +317 -0
  735. package/dist/project/components/FormatNumber/preview.html +2 -0
  736. package/dist/project/components/FormatNumber/stories.js +12 -0
  737. package/dist/project/components/Heading/README.md +240 -0
  738. package/dist/project/components/Heading/preview.html +2 -0
  739. package/dist/project/components/Heading/stories.js +12 -0
  740. package/dist/project/components/HorizontalBarStack/README.md +281 -0
  741. package/dist/project/components/HorizontalBarStack/preview.html +2 -0
  742. package/dist/project/components/HorizontalBarStack/stories.js +12 -0
  743. package/dist/project/components/HttpMethod/README.md +137 -0
  744. package/dist/project/components/HttpMethod/preview.html +2 -0
  745. package/dist/project/components/HttpMethod/stories.js +12 -0
  746. package/dist/project/components/ImageCropper/README.md +332 -0
  747. package/dist/project/components/ImageCropper/preview.html +2 -0
  748. package/dist/project/components/ImageCropper/stories.js +33 -0
  749. package/dist/project/components/Indicator/README.md +110 -0
  750. package/dist/project/components/Indicator/preview.html +2 -0
  751. package/dist/project/components/Indicator/stories.js +12 -0
  752. package/dist/project/components/InlineEdit/README.md +977 -0
  753. package/dist/project/components/InlineEdit/preview.html +2 -0
  754. package/dist/project/components/InlineEdit/stories.js +12 -0
  755. package/dist/project/components/Input/README.md +187 -0
  756. package/dist/project/components/Input/preview.html +2 -0
  757. package/dist/project/components/Input/stories.js +12 -0
  758. package/dist/project/components/InputGroup/README.md +423 -0
  759. package/dist/project/components/InputGroup/preview.html +2 -0
  760. package/dist/project/components/InputGroup/stories.js +12 -0
  761. package/dist/project/components/InputOTP/README.md +201 -0
  762. package/dist/project/components/InputOTP/preview.html +2 -0
  763. package/dist/project/components/InputOTP/stories.js +12 -0
  764. package/dist/project/components/Ip/README.md +226 -0
  765. package/dist/project/components/Ip/preview.html +2 -0
  766. package/dist/project/components/Ip/stories.js +12 -0
  767. package/dist/project/components/Kbd/README.md +126 -0
  768. package/dist/project/components/Kbd/preview.html +2 -0
  769. package/dist/project/components/Kbd/stories.js +12 -0
  770. package/dist/project/components/LineChart/README.md +1479 -0
  771. package/dist/project/components/LineChart/preview.html +2 -0
  772. package/dist/project/components/LineChart/stories.js +12 -0
  773. package/dist/project/components/Link/README.md +160 -0
  774. package/dist/project/components/Link/preview.html +2 -0
  775. package/dist/project/components/Link/stories.js +12 -0
  776. package/dist/project/components/List/README.md +181 -0
  777. package/dist/project/components/List/preview.html +2 -0
  778. package/dist/project/components/List/stories.js +12 -0
  779. package/dist/project/components/Loader/README.md +145 -0
  780. package/dist/project/components/Loader/preview.html +2 -0
  781. package/dist/project/components/Loader/stories.js +12 -0
  782. package/dist/project/components/Logo/README.md +183 -0
  783. package/dist/project/components/Logo/preview.html +2 -0
  784. package/dist/project/components/Logo/stories.js +12 -0
  785. package/dist/project/components/MessageActions/README.md +255 -0
  786. package/dist/project/components/MessageActions/preview.html +2 -0
  787. package/dist/project/components/MessageActions/stories.js +12 -0
  788. package/dist/project/components/Metric/README.md +324 -0
  789. package/dist/project/components/Metric/preview.html +2 -0
  790. package/dist/project/components/Metric/stories.js +12 -0
  791. package/dist/project/components/NavRail/README.md +25 -0
  792. package/dist/project/components/NotificationCenter/README.md +91 -0
  793. package/dist/project/components/NotificationCenter/preview.html +2 -0
  794. package/dist/project/components/NotificationCenter/stories.js +12 -0
  795. package/dist/project/components/NotificationCenter/stories.md +2170 -0
  796. package/dist/project/components/NumberInput/README.md +177 -0
  797. package/dist/project/components/NumberInput/preview.html +2 -0
  798. package/dist/project/components/NumberInput/stories.js +12 -0
  799. package/dist/project/components/NumericBadge/README.md +252 -0
  800. package/dist/project/components/NumericBadge/preview.html +2 -0
  801. package/dist/project/components/NumericBadge/stories.js +12 -0
  802. package/dist/project/components/OverflowList/README.md +324 -0
  803. package/dist/project/components/OverflowList/preview.html +2 -0
  804. package/dist/project/components/OverflowList/stories.js +12 -0
  805. package/dist/project/components/OverflowTooltip/README.md +421 -0
  806. package/dist/project/components/OverflowTooltip/preview.html +2 -0
  807. package/dist/project/components/OverflowTooltip/stories.js +12 -0
  808. package/dist/project/components/Overlay/README.md +95 -0
  809. package/dist/project/components/Overlay/preview.html +2 -0
  810. package/dist/project/components/Overlay/stories.js +12 -0
  811. package/dist/project/components/Overview/README.md +445 -0
  812. package/dist/project/components/Overview/preview.html +2 -0
  813. package/dist/project/components/Overview/stories.js +12 -0
  814. package/dist/project/components/Page/README.md +215 -0
  815. package/dist/project/components/Page/preview.html +2 -0
  816. package/dist/project/components/Page/stories.js +12 -0
  817. package/dist/project/components/Pagination/README.md +302 -0
  818. package/dist/project/components/Pagination/preview.html +2 -0
  819. package/dist/project/components/Pagination/stories.js +12 -0
  820. package/dist/project/components/ParameterPath/README.md +284 -0
  821. package/dist/project/components/ParameterPath/preview.html +2 -0
  822. package/dist/project/components/ParameterPath/stories.js +12 -0
  823. package/dist/project/components/PasswordComplexity/README.md +171 -0
  824. package/dist/project/components/PasswordComplexity/preview.html +2 -0
  825. package/dist/project/components/PasswordComplexity/stories.js +12 -0
  826. package/dist/project/components/PasswordInput/README.md +254 -0
  827. package/dist/project/components/PasswordInput/preview.html +2 -0
  828. package/dist/project/components/PasswordInput/stories.js +12 -0
  829. package/dist/project/components/PieChart/README.md +895 -0
  830. package/dist/project/components/PieChart/preview.html +2 -0
  831. package/dist/project/components/PieChart/stories.js +12 -0
  832. package/dist/project/components/Popover/README.md +282 -0
  833. package/dist/project/components/Popover/preview.html +2 -0
  834. package/dist/project/components/Popover/stories.js +12 -0
  835. package/dist/project/components/Progress/README.md +174 -0
  836. package/dist/project/components/Progress/preview.html +2 -0
  837. package/dist/project/components/Progress/stories.js +12 -0
  838. package/dist/project/components/Radio/README.md +264 -0
  839. package/dist/project/components/Radio/preview.html +2 -0
  840. package/dist/project/components/Radio/stories.js +12 -0
  841. package/dist/project/components/RemoteShell/README.md +1758 -0
  842. package/dist/project/components/RemoteShell/preview.html +2 -0
  843. package/dist/project/components/RemoteShell/stories.js +28 -0
  844. package/dist/project/components/Resizable/README.md +290 -0
  845. package/dist/project/components/Resizable/preview.html +2 -0
  846. package/dist/project/components/Resizable/stories.js +12 -0
  847. package/dist/project/components/ResponseCode/README.md +205 -0
  848. package/dist/project/components/ResponseCode/preview.html +2 -0
  849. package/dist/project/components/ResponseCode/stories.js +12 -0
  850. package/dist/project/components/ScrollArea/README.md +234 -0
  851. package/dist/project/components/ScrollArea/preview.html +2 -0
  852. package/dist/project/components/ScrollArea/stories.js +12 -0
  853. package/dist/project/components/SearchInput/README.md +158 -0
  854. package/dist/project/components/SearchInput/preview.html +2 -0
  855. package/dist/project/components/SearchInput/stories.js +12 -0
  856. package/dist/project/components/SearchModal/README.md +836 -0
  857. package/dist/project/components/SearchModal/preview.html +2 -0
  858. package/dist/project/components/SearchModal/stories.js +12 -0
  859. package/dist/project/components/SegmentedControl/README.md +382 -0
  860. package/dist/project/components/SegmentedControl/preview.html +2 -0
  861. package/dist/project/components/SegmentedControl/stories.js +12 -0
  862. package/dist/project/components/SegmentedTabs/README.md +390 -0
  863. package/dist/project/components/SegmentedTabs/preview.html +2 -0
  864. package/dist/project/components/SegmentedTabs/stories.js +12 -0
  865. package/dist/project/components/Select/README.md +1311 -0
  866. package/dist/project/components/Select/preview.html +2 -0
  867. package/dist/project/components/Select/stories.js +12 -0
  868. package/dist/project/components/Selection/README.md +648 -0
  869. package/dist/project/components/Selection/preview.html +2 -0
  870. package/dist/project/components/Selection/stories.js +12 -0
  871. package/dist/project/components/Separator/README.md +77 -0
  872. package/dist/project/components/Separator/preview.html +2 -0
  873. package/dist/project/components/Separator/stories.js +12 -0
  874. package/dist/project/components/Skeleton/README.md +358 -0
  875. package/dist/project/components/Skeleton/preview.html +2 -0
  876. package/dist/project/components/Skeleton/stories.js +12 -0
  877. package/dist/project/components/Slider/README.md +433 -0
  878. package/dist/project/components/Slider/preview.html +2 -0
  879. package/dist/project/components/Slider/stories.js +12 -0
  880. package/dist/project/components/SplashScreen/README.md +192 -0
  881. package/dist/project/components/SplashScreen/preview.html +2 -0
  882. package/dist/project/components/SplashScreen/stories.js +12 -0
  883. package/dist/project/components/SplitButton/README.md +402 -0
  884. package/dist/project/components/SplitButton/preview.html +2 -0
  885. package/dist/project/components/SplitButton/stories.js +12 -0
  886. package/dist/project/components/Stack/README.md +269 -0
  887. package/dist/project/components/Stack/preview.html +2 -0
  888. package/dist/project/components/Stack/stories.js +12 -0
  889. package/dist/project/components/Stepper/README.md +682 -0
  890. package/dist/project/components/Stepper/preview.html +2 -0
  891. package/dist/project/components/Stepper/stories.js +12 -0
  892. package/dist/project/components/Switch/README.md +236 -0
  893. package/dist/project/components/Switch/preview.html +2 -0
  894. package/dist/project/components/Switch/stories.js +12 -0
  895. package/dist/project/components/Table/README.md +471 -0
  896. package/dist/project/components/Table/preview.html +2 -0
  897. package/dist/project/components/Table/stories.js +12 -0
  898. package/dist/project/components/Table/stories.md +3697 -0
  899. package/dist/project/components/Tabs/README.md +443 -0
  900. package/dist/project/components/Tabs/preview.html +2 -0
  901. package/dist/project/components/Tabs/stories.js +12 -0
  902. package/dist/project/components/Tag/README.md +324 -0
  903. package/dist/project/components/Tag/preview.html +2 -0
  904. package/dist/project/components/Tag/stories.js +12 -0
  905. package/dist/project/components/Text/README.md +200 -0
  906. package/dist/project/components/Text/preview.html +2 -0
  907. package/dist/project/components/Text/stories.js +12 -0
  908. package/dist/project/components/Textarea/README.md +273 -0
  909. package/dist/project/components/Textarea/preview.html +2 -0
  910. package/dist/project/components/Textarea/stories.js +12 -0
  911. package/dist/project/components/TimeInput/README.md +359 -0
  912. package/dist/project/components/TimeInput/preview.html +2 -0
  913. package/dist/project/components/TimeInput/stories.js +12 -0
  914. package/dist/project/components/Timeline/README.md +204 -0
  915. package/dist/project/components/Timeline/preview.html +2 -0
  916. package/dist/project/components/Timeline/stories.js +12 -0
  917. package/dist/project/components/Toast/README.md +718 -0
  918. package/dist/project/components/Toast/preview.html +2 -0
  919. package/dist/project/components/Toast/stories.js +12 -0
  920. package/dist/project/components/ToggleButton/README.md +292 -0
  921. package/dist/project/components/ToggleButton/preview.html +2 -0
  922. package/dist/project/components/ToggleButton/stories.js +12 -0
  923. package/dist/project/components/Tooltip/README.md +199 -0
  924. package/dist/project/components/Tooltip/preview.html +2 -0
  925. package/dist/project/components/Tooltip/stories.js +12 -0
  926. package/dist/project/components/TopHeader/README.md +21 -0
  927. package/dist/project/components/Tour/README.md +532 -0
  928. package/dist/project/components/Tour/preview.html +2 -0
  929. package/dist/project/components/Tour/stories.js +12 -0
  930. package/dist/project/components/Tree/README.md +450 -0
  931. package/dist/project/components/Tree/preview.html +2 -0
  932. package/dist/project/components/Tree/stories.js +12 -0
  933. package/dist/project/components/TreeView/README.md +531 -0
  934. package/dist/project/components/TreeView/preview.html +2 -0
  935. package/dist/project/components/TreeView/stories.js +12 -0
  936. package/dist/project/components/UTILITIES.md +647 -0
  937. package/dist/project/components/UserMessage/README.md +166 -0
  938. package/dist/project/components/UserMessage/preview.html +2 -0
  939. package/dist/project/components/UserMessage/stories.js +12 -0
  940. package/dist/project/components/UtilityPage/README.md +147 -0
  941. package/dist/project/components/UtilityPage/preview.html +2 -0
  942. package/dist/project/components/UtilityPage/stories.js +12 -0
  943. package/dist/project/components/WallyAssistant/README.md +1380 -0
  944. package/dist/project/components/WallyAssistant/preview.html +2 -0
  945. package/dist/project/components/WallyAssistant/stories.js +12 -0
  946. package/dist/project/components/WallyIcon/README.md +130 -0
  947. package/dist/project/components/WallyIcon/preview.html +2 -0
  948. package/dist/project/components/WallyIcon/stories.js +12 -0
  949. package/dist/project/components/WallySkeleton/README.md +96 -0
  950. package/dist/project/components/WallySkeleton/preview.html +2 -0
  951. package/dist/project/components/WallySkeleton/stories.js +12 -0
  952. package/dist/project/components/bundle.css +18734 -0
  953. package/dist/project/components/bundle.js +167 -0
  954. package/dist/project/components/index.d.ts +27226 -0
  955. package/dist/project/design-system.json +35 -0
  956. package/dist/project/fonts/Geist-Variable.woff2 +0 -0
  957. package/dist/project/fonts/GeistMono-Variable.woff2 +0 -0
  958. package/dist/project/fonts/GeistPixel-Square.woff2 +0 -0
  959. package/dist/project/sampler.dc.html +238 -0
  960. package/dist/project/tokens.css +1395 -0
  961. package/dist/project/tokens.json +5235 -0
  962. package/dist/wallarm-sampler.dc.html +239 -0
  963. package/package.json +67 -3
@@ -0,0 +1,3697 @@
1
+ # Table story sources
2
+
3
+ ### Table/Table.stories.tsx
4
+
5
+ ```tsx
6
+ import type { FC } from 'react';
7
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
8
+ import { arrayMove } from '@dnd-kit/sortable';
9
+ import { fn } from 'storybook/test';
10
+ import type { Meta, StoryFn } from 'storybook-react-rsbuild';
11
+ import {
12
+ ChevronDown,
13
+ ChevronRight,
14
+ Copy,
15
+ Database,
16
+ Ellipsis,
17
+ Filter,
18
+ FilterX,
19
+ Lock,
20
+ Plus,
21
+ SearchX,
22
+ Shield,
23
+ Trash2,
24
+ } from '../../icons';
25
+ import { Attribute, AttributeLabel, AttributeValue } from '../Attribute';
26
+ import { Badge } from '../Badge';
27
+ import {
28
+ BulkBarSummaryClear,
29
+ BulkBarSummaryCount,
30
+ BulkBarSummarySelectAll,
31
+ BulkBarSummarySeparator,
32
+ } from '../BulkBar';
33
+ import { Button } from '../Button';
34
+ import { InlineCodeSnippet } from '../CodeSnippet';
35
+ import { Drawer, DrawerBody, DrawerContent, DrawerHeader, DrawerTitle } from '../Drawer';
36
+ import {
37
+ DropdownMenu,
38
+ DropdownMenuContent,
39
+ DropdownMenuContextTrigger,
40
+ DropdownMenuItem,
41
+ DropdownMenuItemContent,
42
+ DropdownMenuItemDescription,
43
+ DropdownMenuItemIcon,
44
+ DropdownMenuItemText,
45
+ DropdownMenuSeparator,
46
+ DropdownMenuTrigger,
47
+ } from '../DropdownMenu';
48
+ import {
49
+ EmptyState,
50
+ EmptyStateActions,
51
+ EmptyStateDescription,
52
+ EmptyStateIllustration,
53
+ EmptyStateMessage,
54
+ EmptyStateTitle,
55
+ } from '../EmptyState';
56
+ import { HttpMethod } from '../HttpMethod';
57
+ import { OverflowList } from '../OverflowList';
58
+ import {
59
+ Pagination,
60
+ PaginationList,
61
+ PaginationNext,
62
+ PaginationPageSize,
63
+ PaginationPrevious,
64
+ useClientPagination,
65
+ } from '../Pagination';
66
+ import type { SelectDataItem } from '../Select';
67
+ import { HStack, VStack } from '../Stack';
68
+ import { Tag } from '../Tag';
69
+ import { Text } from '../Text';
70
+ import { EDITABLE_CELL_COLUMN_META, EditableSelectCell, EditableTextCell } from './EditableCell';
71
+ import {
72
+ createLargeGroupedData,
73
+ createLargeSecurityEvents,
74
+ fullFeaturedColumns,
75
+ groupedHeaderData,
76
+ headerColumnIds,
77
+ headerColumns,
78
+ multiplySecurityEvents,
79
+ renderSecurityPreviewContent,
80
+ renderSecurityPreviewHeader,
81
+ type SecurityEvent,
82
+ type SecurityHeaderEntry,
83
+ securityColumnHelper,
84
+ securityColumnIds,
85
+ securityColumns,
86
+ securityEvents,
87
+ useBidirectionalData,
88
+ useInfiniteData,
89
+ } from './mocks';
90
+ import { Table } from './Table';
91
+ import { TableActionBar, TableActionBarSelection } from './TableActionBar';
92
+ import {
93
+ TableColumnMenu,
94
+ TableColumnMenuHideItem,
95
+ TableColumnMenuMoveLeftItem,
96
+ TableColumnMenuMoveRightItem,
97
+ TableColumnMenuPinItem,
98
+ TableColumnMenuSortItem,
99
+ } from './TableColumnMenu';
100
+ import { TableEmptyState } from './TableEmptyState';
101
+ import {
102
+ TableScrollHandler,
103
+ TableScrollHandlerLeft,
104
+ TableScrollHandlerRight,
105
+ } from './TableScrollHandler';
106
+ import { TableSortTrigger } from './TableSortTrigger';
107
+ import type {
108
+ TableColumnDef,
109
+ TableColumnGroup,
110
+ TableColumnPinningState,
111
+ TableColumnSizingState,
112
+ TableExpandedState,
113
+ TableHandle,
114
+ TableRowReorderEvent,
115
+ TableRowSelectionState,
116
+ TableSortingState,
117
+ TableVisibilityState,
118
+ } from './types';
119
+
120
+ const onDuplicate = fn().mockName('onDuplicate');
121
+ const onDelete = fn().mockName('onDelete');
122
+ const onCopyValue = fn().mockName('onCopyValue');
123
+ const onShowOnly = fn().mockName('onShowOnly');
124
+ const onExclude = fn().mockName('onExclude');
125
+ const onExecute = fn().mockName('onExecute');
126
+
127
+ const DESCRIPTION = [
128
+ 'The grid for many records of one shape — reach for a `Card` grid when a row reads better as an object than as a set of columns, and for `Attribute` when you are showing one record rather than many.',
129
+ 'Everything interactive is controlled: sorting, selection, expansion, column order, pinning, sizing and visibility are each a state prop with a callback, so nothing moves unless you move it, and `manualSorting` hands the sort to the server.',
130
+ 'Filtering belongs above the grid in a `FilterInput`, and a long collection scrolls rather than paginates — `Pagination` is the deliberate exception, for when a page number is part of the task.',
131
+ ].join(' ');
132
+
133
+ const meta = {
134
+ title: 'Data Display/Table',
135
+ component: Table,
136
+ parameters: {
137
+ layout: 'padded',
138
+ docs: { description: { component: DESCRIPTION } },
139
+ },
140
+ tags: ['beta'],
141
+ } satisfies Meta<typeof Table>;
142
+
143
+ export default meta;
144
+
145
+ /**
146
+ * The minimum — `data`, `columns`, `getRowId` and a column to open sorted on. The header stays
147
+ * put as the body scrolls.
148
+ */
149
+ export const Basic: StoryFn<typeof meta> = () => {
150
+ interface Endpoint {
151
+ id: string;
152
+ path: string;
153
+ method: string;
154
+ requests: number;
155
+ status: 'Active' | 'Deprecated';
156
+ }
157
+
158
+ const endpoints: Endpoint[] = [
159
+ { id: '1', path: '/api/v1/users', method: 'GET', requests: 12840, status: 'Active' },
160
+ { id: '2', path: '/api/v1/users/{id}', method: 'PATCH', requests: 3120, status: 'Active' },
161
+ { id: '3', path: '/api/v1/orders', method: 'POST', requests: 8410, status: 'Active' },
162
+ { id: '4', path: '/api/v0/login', method: 'POST', requests: 96, status: 'Deprecated' },
163
+ ];
164
+
165
+ const columns: TableColumnDef<Endpoint>[] = [
166
+ {
167
+ accessorKey: 'path',
168
+ header: 'Endpoint',
169
+ size: 320,
170
+ cell: ({ row }) => (
171
+ <HStack gap={8}>
172
+ <HttpMethod method={row.original.method} />
173
+ <Text size='sm'>{row.original.path}</Text>
174
+ </HStack>
175
+ ),
176
+ },
177
+ {
178
+ accessorKey: 'status',
179
+ header: 'Status',
180
+ size: 140,
181
+ cell: ({ row }) => (
182
+ <Badge
183
+ variant='dotted'
184
+ type='secondary'
185
+ color={row.original.status === 'Active' ? 'green' : 'gray'}
186
+ >
187
+ {row.original.status}
188
+ </Badge>
189
+ ),
190
+ },
191
+ {
192
+ accessorKey: 'requests',
193
+ header: 'Requests',
194
+ size: 120,
195
+ meta: { description: { type: 'tooltip', content: 'Requests in the last 24 hours' } },
196
+ },
197
+ ];
198
+
199
+ const [sorting, setSorting] = useState<TableSortingState>([{ id: 'requests', desc: true }]);
200
+
201
+ return (
202
+ <Table
203
+ aria-label='Endpoints'
204
+ data={endpoints}
205
+ columns={columns}
206
+ getRowId={row => row.id}
207
+ sorting={sorting}
208
+ onSortingChange={setSorting}
209
+ />
210
+ );
211
+ };
212
+
213
+ /**
214
+ * `stretch={false}` — columns keep their defined widths and do not expand to fill the container.
215
+ * Empty space appears on the right when the total column width is less than the container width.
216
+ */
217
+ export const NoStretch: StoryFn<typeof meta> = () => {
218
+ const narrowColumns: TableColumnDef<SecurityEvent>[] = [
219
+ securityColumnHelper.accessor('objectName', {
220
+ header: 'Object name',
221
+ size: 200,
222
+ }),
223
+ securityColumnHelper.accessor('status', {
224
+ header: 'Status',
225
+ size: 120,
226
+ }),
227
+ securityColumnHelper.accessor('requests', {
228
+ header: 'Requests',
229
+ size: 100,
230
+ }),
231
+ ];
232
+
233
+ return (
234
+ <Table data={securityEvents} columns={narrowColumns} getRowId={row => row.id} stretch={false} />
235
+ );
236
+ };
237
+
238
+ // "Rows per page" + page navigation share a right-aligned footer below the table.
239
+ /**
240
+ * `useClientPagination` slices the rows and the pager sits right-aligned below the table,
241
+ * sharing its footer with the rows-per-page control.
242
+ */
243
+ export const WithPagination: StoryFn<typeof meta> = () => {
244
+ const allData = useMemo(() => createLargeSecurityEvents(47), []);
245
+ const { pageData, ...pagination } = useClientPagination(allData, 10);
246
+
247
+ return (
248
+ <VStack gap={12}>
249
+ <Table data={pageData} columns={securityColumns} getRowId={row => row.id} />
250
+ <Pagination {...pagination} align='right' aria-label='Security events'>
251
+ <PaginationPageSize options={[10, 25, 50]} />
252
+ <PaginationPrevious />
253
+ <PaginationList />
254
+ <PaginationNext />
255
+ </Pagination>
256
+ </VStack>
257
+ );
258
+ };
259
+
260
+ /**
261
+ * Sorting held in state, so the arrow and the row order are the same fact — click a header to
262
+ * cycle through the directions.
263
+ */
264
+ export const Sorting: StoryFn<typeof meta> = () => {
265
+ const [sorting, setSorting] = useState<TableSortingState>([]);
266
+
267
+ return (
268
+ <Table
269
+ data={securityEvents}
270
+ columns={securityColumns}
271
+ getRowId={row => row.id}
272
+ sorting={sorting}
273
+ onSortingChange={setSorting}
274
+ />
275
+ );
276
+ };
277
+
278
+ // Demo of server-side sorting: the consumer (this story) owns sort. TanStack
279
+ // only fires `onSortingChange`; we re-derive `data` ourselves to mimic a
280
+ // re-fetch that returns pre-sorted rows.
281
+ const sortSecurityEvents = (sorting: TableSortingState): SecurityEvent[] => {
282
+ if (sorting.length === 0) return securityEvents;
283
+ const { id, desc } = sorting[0]!;
284
+ const key = id as keyof SecurityEvent;
285
+ const direction = desc ? -1 : 1;
286
+ return [...securityEvents].sort((a, b) => {
287
+ const av = a[key];
288
+ const bv = b[key];
289
+ if (av < bv) return -1 * direction;
290
+ if (av > bv) return 1 * direction;
291
+ return 0;
292
+ });
293
+ };
294
+
295
+ /**
296
+ * `manualSorting` stops the table sorting anything itself: it only reports the change, and the
297
+ * story re-derives `data` the way a refetch would.
298
+ */
299
+ export const ManualSorting: StoryFn<typeof meta> = () => {
300
+ const [sorting, setSorting] = useState<TableSortingState>([]);
301
+ const data = useMemo(() => sortSecurityEvents(sorting), [sorting]);
302
+
303
+ return (
304
+ <Table
305
+ data={data}
306
+ columns={securityColumns}
307
+ getRowId={row => row.id}
308
+ sorting={sorting}
309
+ onSortingChange={setSorting}
310
+ manualSorting
311
+ data-testid='manual-sort-table'
312
+ />
313
+ );
314
+ };
315
+
316
+ // Showcase: every analytics seam exposed by `<Table>` today, end-to-end.
317
+ //
318
+ // DOM seams (attribute lands on the real interactive target, document-level
319
+ // SDKs resolve via `closest('[data-analytics-id]')`):
320
+ // • Sort trigger — `<TableSortTrigger column={...} data-analytics-id=...>`
321
+ // rendered as a direct child of `header(ctx)`.
322
+ // • Column header menu — `<TableColumnMenu column={...} data-analytics-id=...>`
323
+ // on the trigger button, plus per-item analytics on
324
+ // `<TableColumnMenuSortItem>`, `<TableColumnMenuPinItem>`,
325
+ // `<TableColumnMenuHideItem>`, `<TableColumnMenuMoveLeftItem>`,
326
+ // `<TableColumnMenuMoveRightItem>`.
327
+ // • Cell content — consumer JSX from `column.cell(ctx)`; analytics
328
+ // sit on the rendered `<a>` / `<button>`.
329
+ // • Per-row actions — consumer JSX from `meta.renderMenuAction(row)`;
330
+ // analytics sit on the trigger and each menu item.
331
+ // • TableActionBar — consumer-rendered bulk action buttons (children).
332
+ // • Selection summary — `<TableActionBarSelection>` override composed from
333
+ // `<BulkBarSummarySelectAll>` / `<BulkBarSummaryClear>`;
334
+ // analytics sit on the real "Select all" / "Clear"
335
+ // `<button>`s (DS keeps toggle / reset wiring).
336
+ // • Scroll controls — `<TableScrollHandler>` as a `Table` child, with
337
+ // `<TableScrollHandlerLeft>` / `<TableScrollHandlerRight>`
338
+ // carrying analytics; portaled into the master header.
339
+ // • Expanded row — consumer JSX from `renderExpandedRow(row)`.
340
+ // • Wrapper testid — `<Table data-testid=...>` lands on the
341
+ // scroll-container `<div>`.
342
+ //
343
+ // Callback seams (state-level signal; richer than a click event because the
344
+ // payload carries the resulting state — direction, visibility, order, etc.):
345
+ // • `onSortingChange(u)` — resulting sort column + direction
346
+ // • `onRowSelectionChange(u)` — selected row map
347
+ // • `onExpandedChange(u)` — expanded row map
348
+ // • `onColumnVisibilityChange(u)` — visibility map (Settings menu)
349
+ // • `onColumnOrderChange(u)` — column order (Settings menu DnD)
350
+ // • `onColumnPinningChange(u)` — pin state (column-header menu)
351
+ // • `onColumnSizingChange(u)` — final width (resize handle)
352
+ // • `onMasterCellClick(rowId)` — master-cell click
353
+ //
354
+ // Pending DS work (no DOM seam yet — callback-only): selection checkbox,
355
+ // expand toggle, master-cell click, resize handle.
356
+ /**
357
+ * Every analytics seam at once — `TableSortTrigger`, `TableColumnMenu` and each of its items,
358
+ * the action bar, the scroll handlers — plus the state callbacks, which carry the resulting
359
+ * sort, selection and column layout rather than just a click.
360
+ */
361
+ export const WithAnalytics: StoryFn<typeof meta> = () => {
362
+ const [sorting, setSorting] = useState<TableSortingState>([]);
363
+ const [rowSelection, setRowSelection] = useState<TableRowSelectionState>({});
364
+ const [expanded, setExpanded] = useState<TableExpandedState>({});
365
+ const [columnVisibility, setColumnVisibility] = useState<TableVisibilityState>({});
366
+ const [columnOrder, setColumnOrder] = useState<string[]>([]);
367
+ const [columnPinning, setColumnPinning] = useState<TableColumnPinningState>({});
368
+ const [columnSizing, setColumnSizing] = useState<TableColumnSizingState>({});
369
+
370
+ // Pretend SDK call — every analytics signal in this story funnels through
371
+ // it so the Storybook actions panel shows one consistent stream.
372
+ const track = useCallback(
373
+ (event: string, props?: Record<string, unknown>) =>
374
+ // biome-ignore lint/suspicious/noConsole: demo story stand-in for an SDK call
375
+ console.info('[analytics]', event, props ?? {}),
376
+ [],
377
+ );
378
+
379
+ const columnsWithAnalytics = useMemo<TableColumnDef<SecurityEvent>[]>(
380
+ () => [
381
+ // Master column — sortable header, link cell with analytics, per-row
382
+ // menu action with analytics on every menu item.
383
+ securityColumnHelper.accessor('objectName', {
384
+ header: ({ column }) => (
385
+ <TableSortTrigger
386
+ column={column}
387
+ data-analytics-id='SECURITY_SORT_OBJECT_NAME'
388
+ data-analytics-props='{"surface":"events-table"}'
389
+ >
390
+ Object name
391
+ </TableSortTrigger>
392
+ ),
393
+ size: 320,
394
+ enableSorting: true,
395
+ meta: {
396
+ sortType: 'text' as const,
397
+ resizeType: 'cut' as const,
398
+ renderMenuAction: row => (
399
+ <DropdownMenu>
400
+ <DropdownMenuTrigger asChild>
401
+ <Button
402
+ variant='ghost'
403
+ color='neutral'
404
+ size='small'
405
+ aria-label='Row actions'
406
+ data-analytics-id='SECURITY_ROW_MENU_OPEN'
407
+ data-analytics-props={`{"rowId":"${row.original.id}"}`}
408
+ >
409
+ <Ellipsis />
410
+ </Button>
411
+ </DropdownMenuTrigger>
412
+ <DropdownMenuContent>
413
+ <DropdownMenuItem
414
+ data-analytics-id='SECURITY_ROW_COPY_NAME'
415
+ data-analytics-props={`{"rowId":"${row.original.id}"}`}
416
+ onSelect={() => navigator.clipboard.writeText(row.original.objectName)}
417
+ >
418
+ <DropdownMenuItemIcon>
419
+ <Copy />
420
+ </DropdownMenuItemIcon>
421
+ <DropdownMenuItemText>Copy name</DropdownMenuItemText>
422
+ </DropdownMenuItem>
423
+ <DropdownMenuSeparator />
424
+ <DropdownMenuItem
425
+ data-analytics-id='SECURITY_ROW_FILTER'
426
+ data-analytics-props={`{"rowId":"${row.original.id}"}`}
427
+ onSelect={() => track('SECURITY_ROW_FILTER_APPLIED', { rowId: row.original.id })}
428
+ >
429
+ <DropdownMenuItemIcon>
430
+ <Filter />
431
+ </DropdownMenuItemIcon>
432
+ <DropdownMenuItemText>Show only</DropdownMenuItemText>
433
+ </DropdownMenuItem>
434
+ <DropdownMenuItem
435
+ data-analytics-id='SECURITY_ROW_EXCLUDE'
436
+ data-analytics-props={`{"rowId":"${row.original.id}"}`}
437
+ onSelect={() => track('SECURITY_ROW_EXCLUDE_APPLIED', { rowId: row.original.id })}
438
+ >
439
+ <DropdownMenuItemIcon>
440
+ <FilterX />
441
+ </DropdownMenuItemIcon>
442
+ <DropdownMenuItemText>Exclude</DropdownMenuItemText>
443
+ </DropdownMenuItem>
444
+ </DropdownMenuContent>
445
+ </DropdownMenu>
446
+ ),
447
+ },
448
+ cell: ({ row }) => (
449
+ <a
450
+ href={`#/events/${row.original.id}`}
451
+ data-analytics-id='SECURITY_ROW_OPEN'
452
+ data-analytics-props={`{"rowId":"${row.original.id}"}`}
453
+ >
454
+ {row.original.objectName}
455
+ </a>
456
+ ),
457
+ }),
458
+ // Other sortable columns — each one declares its own sort target.
459
+ securityColumnHelper.accessor('requests', {
460
+ header: ({ column }) => (
461
+ <TableSortTrigger column={column} data-analytics-id='SECURITY_SORT_REQUESTS'>
462
+ Requests
463
+ </TableSortTrigger>
464
+ ),
465
+ size: 140,
466
+ enableSorting: true,
467
+ }),
468
+ securityColumnHelper.accessor('sourceIp', {
469
+ header: ({ column }) => (
470
+ <TableSortTrigger column={column} data-analytics-id='SECURITY_SORT_SOURCE_IP'>
471
+ Source
472
+ </TableSortTrigger>
473
+ ),
474
+ size: 180,
475
+ enableSorting: true,
476
+ meta: { sortType: 'text' as const },
477
+ }),
478
+ // Status column — demonstrates the column header menu seam with
479
+ // per-item analytics, rendered as a sibling of the sort trigger inside
480
+ // a Fragment.
481
+ securityColumnHelper.accessor('status', {
482
+ header: ({ column }) => (
483
+ <>
484
+ <TableSortTrigger column={column} data-analytics-id='SECURITY_SORT_STATUS'>
485
+ Status
486
+ </TableSortTrigger>
487
+ <TableColumnMenu column={column} data-analytics-id='SECURITY_COLMENU_STATUS'>
488
+ <TableColumnMenuSortItem
489
+ direction='asc'
490
+ data-analytics-id='SECURITY_COLMENU_STATUS_SORT_ASC'
491
+ />
492
+ <TableColumnMenuSortItem
493
+ direction='desc'
494
+ data-analytics-id='SECURITY_COLMENU_STATUS_SORT_DESC'
495
+ />
496
+ <TableColumnMenuPinItem data-analytics-id='SECURITY_COLMENU_STATUS_PIN' />
497
+ <TableColumnMenuHideItem data-analytics-id='SECURITY_COLMENU_STATUS_HIDE' />
498
+ <TableColumnMenuMoveLeftItem data-analytics-id='SECURITY_COLMENU_STATUS_MOVE_LEFT' />
499
+ <TableColumnMenuMoveRightItem data-analytics-id='SECURITY_COLMENU_STATUS_MOVE_RIGHT' />
500
+ </TableColumnMenu>
501
+ </>
502
+ ),
503
+ size: 130,
504
+ enableSorting: true,
505
+ meta: { sortType: 'text' as const, resizeType: 'cut' as const },
506
+ }),
507
+ securityColumnHelper.accessor('firstDetected', {
508
+ header: ({ column }) => (
509
+ <TableSortTrigger column={column} data-analytics-id='SECURITY_SORT_FIRST_DETECTED'>
510
+ First detected
511
+ </TableSortTrigger>
512
+ ),
513
+ size: 160,
514
+ enableSorting: true,
515
+ meta: { sortType: 'date' as const, resizeType: 'cut' as const },
516
+ }),
517
+ ],
518
+ [track],
519
+ );
520
+
521
+ return (
522
+ <Table
523
+ data-testid='security-events-table'
524
+ data={securityEvents}
525
+ columns={columnsWithAnalytics}
526
+ getRowId={row => row.id}
527
+ // Sort: DOM seam on the icon (TableSortTrigger) + callback for direction.
528
+ sorting={sorting}
529
+ onSortingChange={updater => {
530
+ setSorting(prev => {
531
+ const next = typeof updater === 'function' ? updater(prev) : updater;
532
+ track('SECURITY_TABLE_SORT_CHANGED', { columnId: next[0]?.id, desc: !!next[0]?.desc });
533
+ return next;
534
+ });
535
+ }}
536
+ // Selection / expand / settings menu — no DOM seam yet; callbacks carry
537
+ // the resulting state.
538
+ rowSelection={rowSelection}
539
+ onRowSelectionChange={updater => {
540
+ setRowSelection(prev => {
541
+ const next = typeof updater === 'function' ? updater(prev) : updater;
542
+ track('SECURITY_TABLE_SELECTION_CHANGED', { selectedIds: Object.keys(next) });
543
+ return next;
544
+ });
545
+ }}
546
+ expanded={expanded}
547
+ onExpandedChange={updater => {
548
+ setExpanded(prev => {
549
+ const next = typeof updater === 'function' ? updater(prev) : updater;
550
+ track('SECURITY_TABLE_EXPAND_CHANGED', { state: next });
551
+ return next;
552
+ });
553
+ }}
554
+ columnVisibility={columnVisibility}
555
+ onColumnVisibilityChange={updater => {
556
+ setColumnVisibility(prev => {
557
+ const next = typeof updater === 'function' ? updater(prev) : updater;
558
+ track('SECURITY_TABLE_COLUMNS_VISIBILITY_CHANGED', next);
559
+ return next;
560
+ });
561
+ }}
562
+ columnOrder={columnOrder}
563
+ onColumnOrderChange={updater => {
564
+ setColumnOrder(prev => {
565
+ const next = typeof updater === 'function' ? updater(prev) : updater;
566
+ track('SECURITY_TABLE_COLUMNS_REORDERED', { order: next });
567
+ return next;
568
+ });
569
+ }}
570
+ columnPinning={columnPinning}
571
+ onColumnPinningChange={updater => {
572
+ setColumnPinning(prev => {
573
+ const next = typeof updater === 'function' ? updater(prev) : updater;
574
+ track('SECURITY_TABLE_COLUMNS_PINNED', next);
575
+ return next;
576
+ });
577
+ }}
578
+ columnSizing={columnSizing}
579
+ onColumnSizingChange={updater => {
580
+ setColumnSizing(prev => {
581
+ const next = typeof updater === 'function' ? updater(prev) : updater;
582
+ track('SECURITY_TABLE_COLUMN_RESIZED', next);
583
+ return next;
584
+ });
585
+ }}
586
+ onMasterCellClick={rowId => track('SECURITY_TABLE_MASTER_CELL_CLICKED', { rowId })}
587
+ renderExpandedRow={row => (
588
+ <div className='p-16 bg-states-primary-default-alt rounded-8'>
589
+ <HStack justify='between' align='center'>
590
+ <VStack gap={2}>
591
+ <Text size='xs' color='secondary'>
592
+ Source IP
593
+ </Text>
594
+ <Text size='sm'>{row.original.sourceIp}</Text>
595
+ </VStack>
596
+ <Button
597
+ variant='outline'
598
+ color='neutral'
599
+ size='small'
600
+ data-analytics-id='SECURITY_EXPANDED_VIEW_DETAILS'
601
+ data-analytics-props={`{"rowId":"${row.original.id}"}`}
602
+ onClick={() =>
603
+ track('SECURITY_EXPANDED_VIEW_DETAILS_CLICKED', { rowId: row.original.id })
604
+ }
605
+ >
606
+ View full details
607
+ </Button>
608
+ </HStack>
609
+ </div>
610
+ )}
611
+ >
612
+ <TableActionBar>
613
+ {/*
614
+ Selection summary — override the default block to attach analytics to
615
+ the "Select all" / "Clear" controls. The DS keeps the wiring (toggle /
616
+ reset, disabled, count); the consumer onClick runs first and feeds the
617
+ same `track` stream (call preventDefault() to skip the DS action).
618
+ */}
619
+ <TableActionBarSelection>
620
+ <BulkBarSummaryCount />
621
+ <BulkBarSummarySelectAll
622
+ data-analytics-id='SECURITY_BULK_SELECT_ALL'
623
+ onClick={() => track('SECURITY_BULK_SELECT_ALL_CLICKED')}
624
+ />
625
+ <BulkBarSummarySeparator />
626
+ <BulkBarSummaryClear
627
+ data-analytics-id='SECURITY_BULK_CLEAR'
628
+ onClick={() => track('SECURITY_BULK_CLEAR_CLICKED')}
629
+ />
630
+ </TableActionBarSelection>
631
+
632
+ <Button
633
+ variant='ghost'
634
+ color='neutral-alt'
635
+ data-analytics-id='SECURITY_BULK_DUPLICATE'
636
+ data-analytics-props={`{"count":${Object.keys(rowSelection).length}}`}
637
+ onClick={() =>
638
+ track('SECURITY_BULK_DUPLICATE_CLICKED', {
639
+ selectedIds: Object.keys(rowSelection),
640
+ })
641
+ }
642
+ >
643
+ <Copy /> Duplicate
644
+ </Button>
645
+ <Button
646
+ color='destructive'
647
+ data-analytics-id='SECURITY_BULK_DELETE'
648
+ data-analytics-props={`{"count":${Object.keys(rowSelection).length}}`}
649
+ onClick={() =>
650
+ track('SECURITY_BULK_DELETE_CLICKED', {
651
+ selectedIds: Object.keys(rowSelection),
652
+ })
653
+ }
654
+ >
655
+ <Trash2 /> Delete
656
+ </Button>
657
+ </TableActionBar>
658
+
659
+ <TableScrollHandler>
660
+ <TableScrollHandlerLeft data-analytics-id='SECURITY_SCROLL_LEFT' />
661
+ <TableScrollHandlerRight data-analytics-id='SECURITY_SCROLL_RIGHT' />
662
+ </TableScrollHandler>
663
+ </Table>
664
+ );
665
+ };
666
+
667
+ /**
668
+ * `isLoading` with nothing to show yet: skeleton rows under a real header, so the columns do
669
+ * not jump when the data lands.
670
+ */
671
+ export const LoadingState: StoryFn<typeof meta> = () => (
672
+ <Table data={[]} columns={securityColumns} isLoading />
673
+ );
674
+
675
+ /**
676
+ * The same flag with rows already present appends the skeletons below them — this is the shape
677
+ * for fetching the next batch, not for replacing what is on screen.
678
+ */
679
+ export const LoadingWithData: StoryFn<typeof meta> = () => (
680
+ <Table
681
+ className='h-500'
682
+ data={securityEvents}
683
+ columns={securityColumns}
684
+ getRowId={row => row.id}
685
+ isLoading
686
+ />
687
+ );
688
+
689
+ /**
690
+ * `TableEmptyState` holding an `EmptyState` of `type='collection-empty'`: nothing exists yet,
691
+ * so the action creates the first thing.
692
+ */
693
+ export const EmptyCollection: StoryFn<typeof meta> = () => {
694
+ interface Source {
695
+ id: string;
696
+ name: string;
697
+ events: number;
698
+ }
699
+
700
+ const columns: TableColumnDef<Source>[] = [
701
+ { accessorKey: 'name', header: 'Source', size: 320 },
702
+ { accessorKey: 'events', header: 'Events', size: 120 },
703
+ ];
704
+
705
+ return (
706
+ <Table aria-label='Event sources' data={[]} columns={columns} getRowId={row => row.id}>
707
+ <TableEmptyState>
708
+ <EmptyState type='collection-empty'>
709
+ <EmptyStateIllustration>
710
+ <Database />
711
+ </EmptyStateIllustration>
712
+ <EmptyStateMessage>
713
+ <EmptyStateTitle>No events yet</EmptyStateTitle>
714
+ <EmptyStateDescription>
715
+ Security events will appear here as soon as Wallarm detects traffic.
716
+ </EmptyStateDescription>
717
+ </EmptyStateMessage>
718
+ <EmptyStateActions>
719
+ <Button size='medium'>Configure source</Button>
720
+ </EmptyStateActions>
721
+ </EmptyState>
722
+ </TableEmptyState>
723
+ </Table>
724
+ );
725
+ };
726
+
727
+ /**
728
+ * When data exists but the current filters or search matched nothing, stay at
729
+ * the `collection-empty` scale — a table body is a page-level surface, and
730
+ * `no-results` is the compact treatment for a card or menu. What changes is the
731
+ * action: a neutral one that clears the filters, never a primary "create".
732
+ */
733
+ export const NoResults: StoryFn<typeof meta> = () => (
734
+ <Table data={[]} columns={securityColumns} getRowId={row => row.id}>
735
+ <TableEmptyState>
736
+ <EmptyState type='collection-empty'>
737
+ <EmptyStateIllustration>
738
+ <SearchX />
739
+ </EmptyStateIllustration>
740
+ <EmptyStateMessage>
741
+ <EmptyStateTitle>No results found</EmptyStateTitle>
742
+ <EmptyStateDescription>
743
+ No events match your current filters. Try adjusting or resetting them.
744
+ </EmptyStateDescription>
745
+ </EmptyStateMessage>
746
+ <EmptyStateActions>
747
+ <Button size='medium' variant='outline' color='neutral'>
748
+ <FilterX />
749
+ Reset filters
750
+ </Button>
751
+ </EmptyStateActions>
752
+ </EmptyState>
753
+ </TableEmptyState>
754
+ </Table>
755
+ );
756
+
757
+ /**
758
+ * Drag a header edge to resize. `columnSizing` is state like everything else, so a width can
759
+ * be remembered between visits.
760
+ */
761
+ export const ColumnResizing: StoryFn<typeof meta> = () => {
762
+ const [columnSizing, setColumnSizing] = useState<TableColumnSizingState>({});
763
+
764
+ return (
765
+ <Table
766
+ data={securityEvents}
767
+ columns={securityColumns}
768
+ getRowId={row => row.id}
769
+ columnSizing={columnSizing}
770
+ onColumnSizingChange={setColumnSizing}
771
+ />
772
+ );
773
+ };
774
+
775
+ /**
776
+ * A resizable column whose cell holds an `OverflowList`: the tags re-measure as the column
777
+ * narrows instead of being clipped by it.
778
+ */
779
+ export const ColumnResizingWithOverflowList: StoryFn<typeof meta> = () => {
780
+ const [columnSizing, setColumnSizing] = useState<TableColumnSizingState>({});
781
+
782
+ const columns = useMemo<TableColumnDef<SecurityEvent>[]>(
783
+ () => [
784
+ ...securityColumns.slice(0, 1),
785
+ securityColumnHelper.display({
786
+ id: 'tags',
787
+ header: 'Tags',
788
+ size: 240,
789
+ cell: ({ row }) => (
790
+ <OverflowList
791
+ data-testid='tags-overflow-list'
792
+ className='gap-4'
793
+ items={row.original.tags}
794
+ itemRenderer={(item: string) => (
795
+ <Tag key={item} data-testid='tag'>
796
+ {item}
797
+ </Tag>
798
+ )}
799
+ />
800
+ ),
801
+ }),
802
+ ...securityColumns.slice(1),
803
+ ],
804
+ [],
805
+ );
806
+
807
+ return (
808
+ <Table
809
+ data={securityEvents}
810
+ columns={columns}
811
+ getRowId={row => row.id}
812
+ columnSizing={columnSizing}
813
+ onColumnSizingChange={setColumnSizing}
814
+ data-testid='resizing-overflow-table'
815
+ />
816
+ );
817
+ };
818
+
819
+ /**
820
+ * Resizing alongside a pinned column — the pinned one holds its position while the rest of the
821
+ * row moves underneath it.
822
+ */
823
+ export const ColumnResizingWithPinning: StoryFn<typeof meta> = () => {
824
+ const [columnSizing, setColumnSizing] = useState<TableColumnSizingState>({});
825
+ const [columnPinning, setColumnPinning] = useState<TableColumnPinningState>({
826
+ left: ['objectName'],
827
+ });
828
+
829
+ return (
830
+ <Table
831
+ className='max-w-920'
832
+ data={securityEvents}
833
+ columns={securityColumns}
834
+ getRowId={row => row.id}
835
+ columnSizing={columnSizing}
836
+ onColumnSizingChange={setColumnSizing}
837
+ columnPinning={columnPinning}
838
+ onColumnPinningChange={setColumnPinning}
839
+ />
840
+ );
841
+ };
842
+
843
+ /**
844
+ * `columnPinning` freezes a column to the left, so the name of the thing stays readable
845
+ * however far sideways the table scrolls.
846
+ */
847
+ export const ColumnPinning: StoryFn<typeof meta> = () => {
848
+ const [columnPinning, setColumnPinning] = useState<TableColumnPinningState>({
849
+ left: ['objectName'],
850
+ });
851
+
852
+ return (
853
+ <Table
854
+ className='max-w-920'
855
+ data={securityEvents}
856
+ columns={securityColumns}
857
+ getRowId={row => row.id}
858
+ columnPinning={columnPinning}
859
+ onColumnPinningChange={setColumnPinning}
860
+ />
861
+ );
862
+ };
863
+
864
+ /**
865
+ * Drag a header to reorder the columns. `columnOrder` is the same state the settings menu
866
+ * writes, so the two never disagree.
867
+ */
868
+ export const ColumnDragAndDrop: StoryFn<typeof meta> = () => {
869
+ const [columnOrder, setColumnOrder] = useState<string[]>([]);
870
+
871
+ return (
872
+ <Table
873
+ data={securityEvents}
874
+ columns={securityColumns}
875
+ getRowId={row => row.id}
876
+ columnOrder={columnOrder}
877
+ onColumnOrderChange={setColumnOrder}
878
+ />
879
+ );
880
+ };
881
+
882
+ /**
883
+ * The per-column menu that appears on hover — sort, pin, hide, move — which is where the
884
+ * column controls live once a header is too narrow to show them.
885
+ */
886
+ export const ColumnHeaderMenu: StoryFn<typeof meta> = () => {
887
+ const [sorting, setSorting] = useState<TableSortingState>([]);
888
+ const [columnVisibility, setColumnVisibility] = useState<TableVisibilityState>({});
889
+ const [columnPinning, setColumnPinning] = useState<TableColumnPinningState>({});
890
+ const [columnOrder, setColumnOrder] = useState<string[]>([]);
891
+
892
+ return (
893
+ <Table
894
+ data={securityEvents}
895
+ columns={securityColumns}
896
+ getRowId={row => row.id}
897
+ sorting={sorting}
898
+ onSortingChange={setSorting}
899
+ columnVisibility={columnVisibility}
900
+ onColumnVisibilityChange={setColumnVisibility}
901
+ columnPinning={columnPinning}
902
+ onColumnPinningChange={setColumnPinning}
903
+ columnOrder={columnOrder}
904
+ onColumnOrderChange={setColumnOrder}
905
+ />
906
+ );
907
+ };
908
+
909
+ /**
910
+ * Checkboxes plus `TableActionBar`, which rises from the bottom once a row is ticked and
911
+ * carries the verbs that apply to the whole selection.
912
+ */
913
+ export const RowSelection: StoryFn<typeof meta> = () => {
914
+ interface Rule {
915
+ id: string;
916
+ name: string;
917
+ mode: 'Block' | 'Monitor';
918
+ }
919
+
920
+ const columns: TableColumnDef<Rule>[] = [
921
+ { accessorKey: 'name', header: 'Rule', size: 320 },
922
+ {
923
+ accessorKey: 'mode',
924
+ header: 'Mode',
925
+ size: 140,
926
+ cell: ({ row }) => (
927
+ <Badge
928
+ variant='dotted'
929
+ type='secondary'
930
+ color={row.original.mode === 'Block' ? 'red' : 'yellow'}
931
+ >
932
+ {row.original.mode}
933
+ </Badge>
934
+ ),
935
+ },
936
+ ];
937
+
938
+ const [rules, setRules] = useState<Rule[]>([
939
+ { id: '1', name: 'Block SQL injection', mode: 'Block' },
940
+ { id: '2', name: 'Rate limit /login', mode: 'Block' },
941
+ { id: '3', name: 'Watch admin endpoints', mode: 'Monitor' },
942
+ { id: '4', name: 'Mask card numbers', mode: 'Monitor' },
943
+ ]);
944
+ const [rowSelection, setRowSelection] = useState<TableRowSelectionState>({});
945
+
946
+ const handleDuplicate = () => {
947
+ const copies = rules
948
+ .filter(rule => rowSelection[rule.id])
949
+ .map(rule => ({
950
+ ...rule,
951
+ id: `${rule.id}-copy-${rules.length}`,
952
+ name: `${rule.name} (copy)`,
953
+ }));
954
+ setRules([...rules, ...copies]);
955
+ setRowSelection({});
956
+ };
957
+
958
+ const handleDelete = () => {
959
+ // A real app confirms first — see the confirmation-dialogs guideline.
960
+ setRules(rules.filter(rule => !rowSelection[rule.id]));
961
+ setRowSelection({});
962
+ };
963
+
964
+ return (
965
+ <Table
966
+ aria-label='Rules'
967
+ data={rules}
968
+ columns={columns}
969
+ getRowId={row => row.id}
970
+ rowSelection={rowSelection}
971
+ onRowSelectionChange={setRowSelection}
972
+ >
973
+ <TableActionBar>
974
+ <Button variant='ghost' color='neutral-alt' onClick={handleDuplicate}>
975
+ <Copy /> Duplicate
976
+ </Button>
977
+ <Button color='destructive' onClick={handleDelete}>
978
+ <Trash2 /> Delete
979
+ </Button>
980
+ </TableActionBar>
981
+ </Table>
982
+ );
983
+ };
984
+
985
+ /**
986
+ * `onSelectAllRows` separates the master checkbox from a hand-made full selection, which the
987
+ * row state alone cannot tell apart. Here the gesture means "every row matching the filter",
988
+ * including the ones the server has not sent — so the bar jumps to the filter's total, and any
989
+ * manual change drops back to the rows actually loaded.
990
+ */
991
+ export const SelectAllRowsGesture: StoryFn<typeof meta> = () => {
992
+ const [rowSelection, setRowSelection] = useState<TableRowSelectionState>({});
993
+ const [filterWide, setFilterWide] = useState(false);
994
+ const MATCHING_THE_FILTER = 1474;
995
+
996
+ const selectedCount = filterWide ? MATCHING_THE_FILTER : Object.keys(rowSelection).length;
997
+
998
+ return (
999
+ <Table
1000
+ data={securityEvents}
1001
+ columns={securityColumns}
1002
+ getRowId={row => row.id}
1003
+ rowSelection={rowSelection}
1004
+ onRowSelectionChange={updater => {
1005
+ setRowSelection(updater);
1006
+ // "Everything matching the filter, minus this one row" is not a filter,
1007
+ // so any hand-made change falls back to the explicit list.
1008
+ setFilterWide(false);
1009
+ }}
1010
+ onSelectAllRows={setFilterWide}
1011
+ >
1012
+ <TableActionBar>
1013
+ <TableActionBarSelection>
1014
+ <BulkBarSummaryCount>
1015
+ {selectedCount} {selectedCount === 1 ? 'event' : 'events'} selected
1016
+ </BulkBarSummaryCount>
1017
+ <BulkBarSummaryClear />
1018
+ </TableActionBarSelection>
1019
+ <Button color='destructive' onClick={onDelete}>
1020
+ <Trash2 /> Delete
1021
+ </Button>
1022
+ </TableActionBar>
1023
+ </Table>
1024
+ );
1025
+ };
1026
+
1027
+ /**
1028
+ * `enableSelectAllRows={false}` drops the master checkbox while keeping the column, for a
1029
+ * listing where "all rows" is not a scope the consumer can act on. Per-row checkboxes stay,
1030
+ * shift-click range selection included.
1031
+ */
1032
+ export const SelectAllRowsDisabled: StoryFn<typeof meta> = () => {
1033
+ const [rowSelection, setRowSelection] = useState<TableRowSelectionState>({});
1034
+
1035
+ return (
1036
+ <Table
1037
+ data={securityEvents}
1038
+ columns={securityColumns}
1039
+ getRowId={row => row.id}
1040
+ rowSelection={rowSelection}
1041
+ onRowSelectionChange={setRowSelection}
1042
+ enableSelectAllRows={false}
1043
+ />
1044
+ );
1045
+ };
1046
+
1047
+ /**
1048
+ * A selection that outlives the table scrolling away: under `virtualized='window'` the bar is
1049
+ * pinned to the viewport rather than to the table.
1050
+ */
1051
+ export const RowSelectionWindowScroll: StoryFn<typeof meta> = () => {
1052
+ const largeData = useMemo(() => createLargeSecurityEvents(100), []);
1053
+ const [rowSelection, setRowSelection] = useState<TableRowSelectionState>({});
1054
+
1055
+ return (
1056
+ <>
1057
+ <Table
1058
+ data-testid='row-selection-window-scroll-table'
1059
+ data={largeData}
1060
+ columns={securityColumns}
1061
+ getRowId={row => row.id}
1062
+ virtualized='window'
1063
+ rowSelection={rowSelection}
1064
+ onRowSelectionChange={setRowSelection}
1065
+ >
1066
+ <TableActionBar>
1067
+ <Button variant='ghost' color='neutral-alt' onClick={onDuplicate}>
1068
+ <Copy /> Duplicate
1069
+ </Button>
1070
+ <Button color='destructive' onClick={onDelete}>
1071
+ <Trash2 /> Delete
1072
+ </Button>
1073
+ </TableActionBar>
1074
+ </Table>
1075
+ {/* Filler so the e2e suite can scroll the table fully out of view while a
1076
+ selection stays active — this story's only content otherwise fills
1077
+ exactly to the table's own height, so max scroll would always leave
1078
+ the table's tail in view. Generous margin over the viewport height
1079
+ (800px in the e2e config) so the anchor is unambiguously out of view
1080
+ at max scroll, not just mostly. */}
1081
+ <div style={{ height: 3000 }} />
1082
+ </>
1083
+ );
1084
+ };
1085
+ RowSelectionWindowScroll.tags = ['!autodocs'];
1086
+
1087
+ /**
1088
+ * `getSubRows` turns flat data into groups — each group row counts its children and collapses
1089
+ * them, keyed in the same `expanded` state a row detail uses.
1090
+ */
1091
+ export const RowGrouping: StoryFn<typeof meta> = () => {
1092
+ const [expanded, setExpanded] = useState<TableExpandedState>({ 'group-cto': true });
1093
+ const [sorting, setSorting] = useState<TableSortingState>([{ id: 'lastEdited', desc: true }]);
1094
+ const [rowSelection, setRowSelection] = useState<TableRowSelectionState>({});
1095
+
1096
+ return (
1097
+ <Table<SecurityHeaderEntry>
1098
+ data={groupedHeaderData}
1099
+ columns={headerColumns}
1100
+ getRowId={row => row.id}
1101
+ getSubRows={row => row.children}
1102
+ expanded={expanded}
1103
+ onExpandedChange={setExpanded}
1104
+ sorting={sorting}
1105
+ onSortingChange={setSorting}
1106
+ rowSelection={rowSelection}
1107
+ onRowSelectionChange={setRowSelection}
1108
+ />
1109
+ );
1110
+ };
1111
+
1112
+ /**
1113
+ * `renderExpandedRow` draws a panel beneath the row, for the two or three fields that do not
1114
+ * deserve columns of their own.
1115
+ */
1116
+ export const RowExpanding: StoryFn<typeof meta> = () => {
1117
+ const [expanded, setExpanded] = useState<TableExpandedState>({});
1118
+
1119
+ return (
1120
+ <Table
1121
+ data={securityEvents}
1122
+ columns={securityColumns}
1123
+ getRowId={row => row.id}
1124
+ expanded={expanded}
1125
+ onExpandedChange={setExpanded}
1126
+ renderExpandedRow={row => (
1127
+ <div className='p-16 bg-states-primary-default-alt rounded-8'>
1128
+ <Text size='sm' weight='medium'>
1129
+ Event Details
1130
+ </Text>
1131
+ <div className='grid grid-cols-3 gap-8 mt-8'>
1132
+ <VStack gap={2}>
1133
+ <Text size='xs' color='secondary'>
1134
+ Source IP
1135
+ </Text>
1136
+ <Text size='sm'>{row.original.sourceIp}</Text>
1137
+ </VStack>
1138
+ <VStack gap={2}>
1139
+ <Text size='xs' color='secondary'>
1140
+ Provider
1141
+ </Text>
1142
+ <Text size='sm'>{row.original.sourceProvider}</Text>
1143
+ </VStack>
1144
+ <VStack gap={2}>
1145
+ <Text size='xs' color='secondary'>
1146
+ Endpoint
1147
+ </Text>
1148
+ <HStack gap={4}>
1149
+ <HttpMethod method={row.original.endpointMethod} />
1150
+ <Text size='sm'>{row.original.endpointPath}</Text>
1151
+ </HStack>
1152
+ </VStack>
1153
+ </div>
1154
+ </div>
1155
+ )}
1156
+ />
1157
+ );
1158
+ };
1159
+
1160
+ /**
1161
+ * Right-click inside a cell: `DropdownMenuContextTrigger` puts that column's verbs where the
1162
+ * pointer already is, so each column can offer its own.
1163
+ */
1164
+ export const ContextMenu: StoryFn<typeof meta> = () => {
1165
+ const contextMenuColumns: TableColumnDef<(typeof securityEvents)[number]>[] = [
1166
+ securityColumnHelper.accessor('objectName', {
1167
+ header: 'Object name',
1168
+ cell: ({ getValue }) => (
1169
+ <DropdownMenu>
1170
+ <DropdownMenuContextTrigger>
1171
+ <Text size='sm'>{getValue()}</Text>
1172
+ </DropdownMenuContextTrigger>
1173
+ <DropdownMenuContent>
1174
+ <DropdownMenuItem onSelect={onCopyValue}>
1175
+ <DropdownMenuItemIcon>
1176
+ <Copy />
1177
+ </DropdownMenuItemIcon>
1178
+ <DropdownMenuItemText>Copy value</DropdownMenuItemText>
1179
+ </DropdownMenuItem>
1180
+
1181
+ <DropdownMenuSeparator />
1182
+
1183
+ <DropdownMenuItem onSelect={onShowOnly}>
1184
+ <DropdownMenuItemIcon>
1185
+ <Filter />
1186
+ </DropdownMenuItemIcon>
1187
+ <DropdownMenuItemContent>
1188
+ <DropdownMenuItemText>Show only</DropdownMenuItemText>
1189
+ <DropdownMenuItemDescription>Filter to matching rows</DropdownMenuItemDescription>
1190
+ </DropdownMenuItemContent>
1191
+ </DropdownMenuItem>
1192
+
1193
+ <DropdownMenuItem onSelect={onExclude}>
1194
+ <DropdownMenuItemIcon>
1195
+ <FilterX />
1196
+ </DropdownMenuItemIcon>
1197
+ <DropdownMenuItemContent>
1198
+ <DropdownMenuItemText>Exclude</DropdownMenuItemText>
1199
+ <DropdownMenuItemDescription>Filter out matching rows</DropdownMenuItemDescription>
1200
+ </DropdownMenuItemContent>
1201
+ </DropdownMenuItem>
1202
+ </DropdownMenuContent>
1203
+ </DropdownMenu>
1204
+ ),
1205
+ }),
1206
+ securityColumnHelper.accessor('parameter', {
1207
+ header: 'Parameters',
1208
+ cell: ({ getValue }) => (
1209
+ <DropdownMenu>
1210
+ <DropdownMenuContextTrigger>
1211
+ <InlineCodeSnippet code={getValue()} size='sm' copyable={false} />
1212
+ </DropdownMenuContextTrigger>
1213
+ <DropdownMenuContent>
1214
+ <DropdownMenuItem onSelect={onExecute}>
1215
+ <DropdownMenuItemIcon>
1216
+ <Copy />
1217
+ </DropdownMenuItemIcon>
1218
+ <DropdownMenuItemText>Execute</DropdownMenuItemText>
1219
+ </DropdownMenuItem>
1220
+ </DropdownMenuContent>
1221
+ </DropdownMenu>
1222
+ ),
1223
+ }),
1224
+ securityColumnHelper.accessor('status', {
1225
+ header: 'Status',
1226
+ cell: ({ getValue }) => {
1227
+ const status = getValue();
1228
+ return (
1229
+ <Badge
1230
+ variant='dotted'
1231
+ color={status === 'Blocked' ? 'red' : 'yellow'}
1232
+ type='secondary'
1233
+ size='medium'
1234
+ >
1235
+ {status}
1236
+ </Badge>
1237
+ );
1238
+ },
1239
+ }),
1240
+ securityColumnHelper.accessor('cweId', {
1241
+ header: 'Security info',
1242
+ cell: ({ getValue }) => (
1243
+ <Badge color='rose' type='secondary' size='medium' textVariant='code'>
1244
+ {getValue()}
1245
+ </Badge>
1246
+ ),
1247
+ }),
1248
+ ];
1249
+
1250
+ return <Table data={securityEvents} columns={contextMenuColumns} getRowId={row => row.id} />;
1251
+ };
1252
+
1253
+ /**
1254
+ * The gear at the end of the header — search the columns, show and hide them, reorder them,
1255
+ * reset — writing the same visibility and order state as the header menu.
1256
+ */
1257
+ export const SettingsMenu: StoryFn<typeof meta> = () => {
1258
+ const [columnVisibility, setColumnVisibility] = useState<TableVisibilityState>({});
1259
+ const [columnOrder, setColumnOrder] = useState<string[]>([]);
1260
+
1261
+ return (
1262
+ <Table
1263
+ data={securityEvents}
1264
+ columns={securityColumns}
1265
+ getRowId={row => row.id}
1266
+ columnVisibility={columnVisibility}
1267
+ onColumnVisibilityChange={setColumnVisibility}
1268
+ columnOrder={columnOrder}
1269
+ onColumnOrderChange={setColumnOrder}
1270
+ defaultColumnVisibility={{}}
1271
+ defaultColumnOrder={securityColumnIds}
1272
+ />
1273
+ );
1274
+ };
1275
+
1276
+ /**
1277
+ * `deferColumnVisibility` holds switch toggles and "Reset to default" while the
1278
+ * settings menu is open and applies them as one `columnVisibility` change on
1279
+ * close — for consumers that fetch a column's data only once the menu closes,
1280
+ * so a new column never sits empty behind the open menu.
1281
+ */
1282
+ export const SettingsMenuDeferred: StoryFn<typeof meta> = () => {
1283
+ const [columnVisibility, setColumnVisibility] = useState<TableVisibilityState>({});
1284
+ const [columnOrder, setColumnOrder] = useState<string[]>([]);
1285
+
1286
+ return (
1287
+ <Table
1288
+ data={securityEvents}
1289
+ columns={securityColumns}
1290
+ getRowId={row => row.id}
1291
+ columnVisibility={columnVisibility}
1292
+ onColumnVisibilityChange={setColumnVisibility}
1293
+ columnOrder={columnOrder}
1294
+ onColumnOrderChange={setColumnOrder}
1295
+ defaultColumnVisibility={{}}
1296
+ defaultColumnOrder={securityColumnIds}
1297
+ deferColumnVisibility
1298
+ />
1299
+ );
1300
+ };
1301
+
1302
+ /**
1303
+ * `columnGroups` turns the settings menu into labeled, non-reorderable sections
1304
+ * (the FilterInput field-menu pattern): no drag handle, columns rendered under
1305
+ * their group header in the given order. Column ids absent from every group
1306
+ * (`cweId` here) fall into a trailing headerless section.
1307
+ */
1308
+ export const SettingsMenuGrouped: StoryFn<typeof meta> = () => {
1309
+ const [columnVisibility, setColumnVisibility] = useState<TableVisibilityState>({});
1310
+
1311
+ const columnGroups: TableColumnGroup[] = [
1312
+ {
1313
+ label: 'Threat classification',
1314
+ columns: ['objectName', 'status', 'firstDetected', 'lastSeen'],
1315
+ },
1316
+ { label: 'Request features', columns: ['sourceIp', 'parameter', 'endpoint', 'requests'] },
1317
+ ];
1318
+
1319
+ return (
1320
+ <Table
1321
+ data={securityEvents}
1322
+ columns={securityColumns}
1323
+ getRowId={row => row.id}
1324
+ columnVisibility={columnVisibility}
1325
+ onColumnVisibilityChange={setColumnVisibility}
1326
+ columnGroups={columnGroups}
1327
+ />
1328
+ );
1329
+ };
1330
+
1331
+ /**
1332
+ * `virtualized='container'` inside a fixed height renders only the rows in view, which is what
1333
+ * lets a thousand rows behave like ten.
1334
+ */
1335
+ export const Virtualization: StoryFn<typeof meta> = () => {
1336
+ const largeData = useMemo(() => createLargeSecurityEvents(1000), []);
1337
+
1338
+ return (
1339
+ <Table
1340
+ className='h-500'
1341
+ data={largeData}
1342
+ columns={securityColumns}
1343
+ getRowId={row => row.id}
1344
+ virtualized='container'
1345
+ />
1346
+ );
1347
+ };
1348
+ Virtualization.tags = ['!autodocs'];
1349
+
1350
+ /**
1351
+ * `virtualized='window'` measures against the page's own scroll instead, for a table that is
1352
+ * the page rather than a panel inside one.
1353
+ */
1354
+ export const WindowVirtualization: StoryFn<typeof meta> = () => {
1355
+ const largeData = useMemo(() => createLargeSecurityEvents(1000), []);
1356
+ const [columnOrder, setColumnOrder] = useState<string[]>([]);
1357
+ const [columnSizing, setColumnSizing] = useState<TableColumnSizingState>({});
1358
+
1359
+ return (
1360
+ <Table
1361
+ data={largeData}
1362
+ columns={securityColumns}
1363
+ getRowId={row => row.id}
1364
+ virtualized='window'
1365
+ columnOrder={columnOrder}
1366
+ onColumnOrderChange={setColumnOrder}
1367
+ columnSizing={columnSizing}
1368
+ onColumnSizingChange={setColumnSizing}
1369
+ />
1370
+ );
1371
+ };
1372
+ WindowVirtualization.tags = ['!autodocs'];
1373
+
1374
+ /**
1375
+ * The imperative escape hatch: `scrollToRow(id, { align })` off the ref, returning false for
1376
+ * an id the table does not have so a deep link can fall back.
1377
+ */
1378
+ export const ScrollToRow: StoryFn<typeof meta> = () => {
1379
+ const largeData = useMemo(() => createLargeSecurityEvents(1000), []);
1380
+ const tableRef = useRef<TableHandle>(null);
1381
+ const [lastResult, setLastResult] = useState<string | null>(null);
1382
+
1383
+ const scroll = (index: number, align: 'start' | 'center' | 'end' | 'auto' = 'center') => {
1384
+ const row = largeData[index];
1385
+ if (!row) return;
1386
+ const ok = tableRef.current?.scrollToRow(row.id, { align, behavior: 'smooth' }) ?? false;
1387
+ setLastResult(`scrollToRow("${row.id}", { align: "${align}" }) → ${ok}`);
1388
+ };
1389
+
1390
+ return (
1391
+ <VStack gap={8}>
1392
+ <HStack gap={8}>
1393
+ <Button variant='outline' color='neutral' onClick={() => scroll(0, 'start')}>
1394
+ Top
1395
+ </Button>
1396
+ <Button variant='outline' color='neutral' onClick={() => scroll(499, 'center')}>
1397
+ Middle
1398
+ </Button>
1399
+ <Button variant='outline' color='neutral' onClick={() => scroll(999, 'end')}>
1400
+ Bottom
1401
+ </Button>
1402
+ <Button
1403
+ variant='secondary'
1404
+ color='neutral'
1405
+ onClick={() => {
1406
+ const ok = tableRef.current?.scrollToRow('does-not-exist') ?? false;
1407
+ setLastResult(`scrollToRow("does-not-exist") → ${ok}`);
1408
+ }}
1409
+ >
1410
+ Missing id (returns false)
1411
+ </Button>
1412
+ </HStack>
1413
+ {lastResult && (
1414
+ <Text size='sm' color='secondary'>
1415
+ {lastResult}
1416
+ </Text>
1417
+ )}
1418
+ <Table
1419
+ ref={tableRef}
1420
+ className='h-500'
1421
+ data={largeData}
1422
+ columns={securityColumns}
1423
+ getRowId={row => row.id}
1424
+ virtualized='container'
1425
+ />
1426
+ </VStack>
1427
+ );
1428
+ };
1429
+ ScrollToRow.tags = ['!autodocs'];
1430
+
1431
+ /**
1432
+ * `onEndReached` with `isLoading` — the house default for a long collection, where rows arrive
1433
+ * as you reach them and there is no page number to lose.
1434
+ */
1435
+ export const InfiniteScroll: StoryFn<typeof meta> = () => {
1436
+ const { data, isFetching, hasMore, totalItems, fetchNextPage } = useInfiniteData();
1437
+
1438
+ return (
1439
+ <VStack gap={8}>
1440
+ <Text size='sm' color='secondary'>
1441
+ Loaded {data.length} of {totalItems} rows {isFetching && '— loading...'}
1442
+ {!hasMore && ' — all loaded'}
1443
+ </Text>
1444
+ <Table
1445
+ className='h-500'
1446
+ data={data}
1447
+ columns={securityColumns}
1448
+ getRowId={row => row.id}
1449
+ virtualized='container'
1450
+ isLoading={isFetching}
1451
+ onEndReached={fetchNextPage}
1452
+ onEndReachedThreshold={200}
1453
+ />
1454
+ </VStack>
1455
+ );
1456
+ };
1457
+ InfiniteScroll.tags = ['!autodocs'];
1458
+
1459
+ /**
1460
+ * The same fetch-on-approach against the page's scroll, where the trigger has to account for
1461
+ * everything else on the page as well as the table.
1462
+ */
1463
+ export const InfiniteScrollWindow: StoryFn<typeof meta> = () => {
1464
+ const { data, isFetching, hasMore, totalItems, fetchNextPage } = useInfiniteData();
1465
+
1466
+ return (
1467
+ <VStack gap={8}>
1468
+ <Text size='sm' color='secondary'>
1469
+ Loaded {data.length} of {totalItems} rows {isFetching && '— loading...'}
1470
+ {!hasMore && ' — all loaded'}
1471
+ </Text>
1472
+ <Table
1473
+ data={data}
1474
+ columns={securityColumns}
1475
+ getRowId={row => row.id}
1476
+ virtualized='window'
1477
+ isLoading={isFetching}
1478
+ onEndReached={fetchNextPage}
1479
+ onEndReachedThreshold={300}
1480
+ />
1481
+ </VStack>
1482
+ );
1483
+ };
1484
+ InfiniteScrollWindow.tags = ['!autodocs'];
1485
+
1486
+ /**
1487
+ * Both ends at once: `onStartReached` prepends, and the scroll position is compensated so the
1488
+ * row you were reading does not slide out from under you.
1489
+ */
1490
+ export const BidirectionalInfiniteScroll: StoryFn<typeof meta> = () => {
1491
+ const {
1492
+ data,
1493
+ anchorId,
1494
+ isFetchingPrev,
1495
+ isFetchingNext,
1496
+ hasPrev,
1497
+ hasNext,
1498
+ fetchPrevPage,
1499
+ fetchNextPage,
1500
+ } = useBidirectionalData();
1501
+
1502
+ return (
1503
+ <VStack gap={8}>
1504
+ <Text size='sm' color='secondary'>
1505
+ Window of {data.length} rows around the anchor — scroll up for top skeletons, down for
1506
+ bottom ones{(isFetchingPrev || isFetchingNext) && ' — loading...'}
1507
+ {!hasPrev && ' — top reached'}
1508
+ {!hasNext && ' — bottom reached'}
1509
+ </Text>
1510
+ <Table
1511
+ className='h-500'
1512
+ data={data}
1513
+ columns={securityColumns}
1514
+ getRowId={row => row.id}
1515
+ virtualized='container'
1516
+ isLoading={isFetchingNext}
1517
+ isLoadingPrevious={isFetchingPrev}
1518
+ initialScrollToRowId={anchorId}
1519
+ onStartReached={fetchPrevPage}
1520
+ onStartReachedThreshold={200}
1521
+ onEndReached={fetchNextPage}
1522
+ onEndReachedThreshold={200}
1523
+ />
1524
+ </VStack>
1525
+ );
1526
+ };
1527
+ BidirectionalInfiniteScroll.tags = ['!autodocs'];
1528
+
1529
+ // Same dataset/anchor as BidirectionalInfiniteScroll, but the document itself
1530
+ // scrolls — the regression mode for prepend compensation, where unrelated page
1531
+ // content shares the scrollHeight the old delta diffed against.
1532
+ /**
1533
+ * The prepend case against the document scroll, where unrelated page content shares the height
1534
+ * being measured — the regression this story exists to catch.
1535
+ */
1536
+ export const BidirectionalInfiniteScrollWindow: StoryFn<typeof meta> = () => {
1537
+ const {
1538
+ data,
1539
+ anchorId,
1540
+ isFetchingPrev,
1541
+ isFetchingNext,
1542
+ hasPrev,
1543
+ hasNext,
1544
+ fetchPrevPage,
1545
+ fetchNextPage,
1546
+ } = useBidirectionalData();
1547
+
1548
+ return (
1549
+ <VStack gap={8}>
1550
+ <Text size='sm' color='secondary'>
1551
+ Window of {data.length} rows around the anchor
1552
+ {(isFetchingPrev || isFetchingNext) && ' — loading...'}
1553
+ {!hasPrev && ' — top reached'}
1554
+ {!hasNext && ' — bottom reached'}
1555
+ </Text>
1556
+ <Table
1557
+ data={data}
1558
+ columns={securityColumns}
1559
+ getRowId={row => row.id}
1560
+ virtualized='window'
1561
+ isLoading={isFetchingNext}
1562
+ isLoadingPrevious={isFetchingPrev}
1563
+ initialScrollToRowId={anchorId}
1564
+ onStartReached={fetchPrevPage}
1565
+ onStartReachedThreshold={200}
1566
+ onEndReached={fetchNextPage}
1567
+ onEndReachedThreshold={200}
1568
+ />
1569
+ </VStack>
1570
+ );
1571
+ };
1572
+ BidirectionalInfiniteScrollWindow.tags = ['!autodocs'];
1573
+
1574
+ /**
1575
+ * `virtualized='window'` in an app shell that scrolls a content pane instead of the document: the
1576
+ * table follows the pane. The `overflow-x: hidden` wrapper in between never scrolls (its
1577
+ * `overflow-y` only computes to `auto`), so it must not be taken for the scroll root.
1578
+ */
1579
+ export const InfiniteScrollWindowInPane: StoryFn<typeof meta> = () => {
1580
+ const { data, isFetchingPrev, isFetchingNext, hasPrev, hasNext, fetchPrevPage, fetchNextPage } =
1581
+ useBidirectionalData();
1582
+
1583
+ return (
1584
+ <div data-testid='scroll-pane' style={{ height: 480, overflowY: 'auto' }}>
1585
+ <div style={{ overflowX: 'hidden' }}>
1586
+ <VStack gap={8}>
1587
+ <Text size='sm' color='secondary'>
1588
+ Window of {data.length} rows around the anchor
1589
+ {(isFetchingPrev || isFetchingNext) && ' — loading...'}
1590
+ {!hasPrev && ' — top reached'}
1591
+ {!hasNext && ' — bottom reached'}
1592
+ </Text>
1593
+ <Table
1594
+ data={data}
1595
+ columns={securityColumns}
1596
+ getRowId={row => row.id}
1597
+ virtualized='window'
1598
+ isLoading={isFetchingNext}
1599
+ isLoadingPrevious={isFetchingPrev}
1600
+ onStartReached={fetchPrevPage}
1601
+ onStartReachedThreshold={200}
1602
+ onEndReached={fetchNextPage}
1603
+ onEndReachedThreshold={200}
1604
+ />
1605
+ </VStack>
1606
+ </div>
1607
+ </div>
1608
+ );
1609
+ };
1610
+ InfiniteScrollWindowInPane.tags = ['!autodocs'];
1611
+
1612
+ /**
1613
+ * A share-link landing in a pane host: the window-mode table opens scrolled to its anchor row,
1614
+ * inside the pane — the document stays put.
1615
+ */
1616
+ export const InfiniteScrollWindowInPaneAnchored: StoryFn<typeof meta> = () => {
1617
+ const { data, anchorId, isFetchingPrev, isFetchingNext, fetchPrevPage, fetchNextPage } =
1618
+ useBidirectionalData();
1619
+
1620
+ return (
1621
+ <div data-testid='scroll-pane' style={{ height: 480, overflowY: 'auto' }}>
1622
+ <Table
1623
+ data={data}
1624
+ columns={securityColumns}
1625
+ getRowId={row => row.id}
1626
+ virtualized='window'
1627
+ isLoading={isFetchingNext}
1628
+ isLoadingPrevious={isFetchingPrev}
1629
+ initialScrollToRowId={anchorId}
1630
+ onStartReached={fetchPrevPage}
1631
+ onStartReachedThreshold={200}
1632
+ onEndReached={fetchNextPage}
1633
+ onEndReachedThreshold={200}
1634
+ />
1635
+ </div>
1636
+ );
1637
+ };
1638
+ InfiniteScrollWindowInPaneAnchored.tags = ['!autodocs'];
1639
+
1640
+ /**
1641
+ * A pane host that mounts the table before its data: rows arrive after a delay. Neither edge may
1642
+ * fire against the window meanwhile — only the edge the user scrolls to.
1643
+ */
1644
+ export const InfiniteScrollWindowInPaneLateRows: StoryFn<typeof meta> = () => {
1645
+ const { data, isFetchingPrev, isFetchingNext, fetchPrevPage, fetchNextPage } =
1646
+ useBidirectionalData();
1647
+ const [hasLoaded, setHasLoaded] = useState(false);
1648
+ const [calls, setCalls] = useState({ start: 0, end: 0 });
1649
+ useEffect(() => {
1650
+ const timer = setTimeout(() => setHasLoaded(true), 500);
1651
+ return () => clearTimeout(timer);
1652
+ }, []);
1653
+
1654
+ return (
1655
+ <div data-testid='scroll-pane' style={{ height: 480, overflowY: 'auto' }}>
1656
+ <Text size='sm' color='secondary'>
1657
+ Edge calls: start {calls.start}, end {calls.end}
1658
+ </Text>
1659
+ <Table
1660
+ data={hasLoaded ? data : []}
1661
+ columns={securityColumns}
1662
+ getRowId={row => row.id}
1663
+ virtualized='window'
1664
+ isLoading={!hasLoaded || isFetchingNext}
1665
+ isLoadingPrevious={isFetchingPrev}
1666
+ // Nothing to page from before the first page — as with a real cursor.
1667
+ onStartReached={
1668
+ hasLoaded
1669
+ ? () => {
1670
+ setCalls(prev => ({ ...prev, start: prev.start + 1 }));
1671
+ fetchPrevPage();
1672
+ }
1673
+ : undefined
1674
+ }
1675
+ onStartReachedThreshold={200}
1676
+ onEndReached={
1677
+ hasLoaded
1678
+ ? () => {
1679
+ setCalls(prev => ({ ...prev, end: prev.end + 1 }));
1680
+ fetchNextPage();
1681
+ }
1682
+ : undefined
1683
+ }
1684
+ onEndReachedThreshold={200}
1685
+ />
1686
+ </div>
1687
+ );
1688
+ };
1689
+ InfiniteScrollWindowInPaneLateRows.tags = ['!autodocs'];
1690
+
1691
+ /**
1692
+ * A share-link landing in a pane that starts hidden (a closed tab or drawer): the anchor waits
1693
+ * until the table is shown and laid out, then lands inside the pane.
1694
+ */
1695
+ export const InfiniteScrollWindowInHiddenPaneAnchored: StoryFn<typeof meta> = () => {
1696
+ const [isShown, setIsShown] = useState(false);
1697
+ const { data, anchorId, isFetchingPrev, isFetchingNext, fetchPrevPage, fetchNextPage } =
1698
+ useBidirectionalData();
1699
+
1700
+ return (
1701
+ <VStack gap={8}>
1702
+ <Button data-testid='show-table' onClick={() => setIsShown(true)}>
1703
+ Show table
1704
+ </Button>
1705
+ <div
1706
+ data-testid='scroll-pane'
1707
+ style={{ height: 480, overflowY: 'auto', display: isShown ? 'block' : 'none' }}
1708
+ >
1709
+ <Table
1710
+ data={data}
1711
+ columns={securityColumns}
1712
+ getRowId={row => row.id}
1713
+ virtualized='window'
1714
+ isLoading={isFetchingNext}
1715
+ isLoadingPrevious={isFetchingPrev}
1716
+ initialScrollToRowId={anchorId}
1717
+ onStartReached={fetchPrevPage}
1718
+ onStartReachedThreshold={200}
1719
+ onEndReached={fetchNextPage}
1720
+ onEndReachedThreshold={200}
1721
+ />
1722
+ </div>
1723
+ </VStack>
1724
+ );
1725
+ };
1726
+ InfiniteScrollWindowInHiddenPaneAnchored.tags = ['!autodocs'];
1727
+
1728
+ /**
1729
+ * A window-mode table mounting into a pane the user already scrolled (content above it): the pane
1730
+ * keeps its position — adopting the root must not reset it.
1731
+ */
1732
+ export const InfiniteScrollWindowInScrolledPane: StoryFn<typeof meta> = () => {
1733
+ const [isShown, setIsShown] = useState(false);
1734
+ const { data, isFetchingNext, fetchNextPage } = useBidirectionalData();
1735
+
1736
+ return (
1737
+ <VStack gap={8}>
1738
+ <Button data-testid='show-table' onClick={() => setIsShown(true)}>
1739
+ Show table
1740
+ </Button>
1741
+ <div data-testid='scroll-pane' style={{ height: 480, overflowY: 'auto' }}>
1742
+ <div style={{ height: 800 }}>
1743
+ <Text size='sm' color='secondary'>
1744
+ Content above the table
1745
+ </Text>
1746
+ </div>
1747
+ {isShown && (
1748
+ <Table
1749
+ data={data}
1750
+ columns={securityColumns}
1751
+ getRowId={row => row.id}
1752
+ virtualized='window'
1753
+ isLoading={isFetchingNext}
1754
+ onEndReached={fetchNextPage}
1755
+ onEndReachedThreshold={200}
1756
+ />
1757
+ )}
1758
+ </div>
1759
+ </VStack>
1760
+ );
1761
+ };
1762
+ InfiniteScrollWindowInScrolledPane.tags = ['!autodocs'];
1763
+
1764
+ /**
1765
+ * A column can explain itself: `description` renders as a second line under the title, or as a
1766
+ * tooltip behind a dashed underline when the header has no room for one.
1767
+ */
1768
+ export const HeaderColumnDescription: StoryFn<typeof meta> = () => {
1769
+ const [sorting, setSorting] = useState<TableSortingState>([]);
1770
+
1771
+ const columns = useMemo<TableColumnDef<(typeof securityEvents)[number]>[]>(
1772
+ () =>
1773
+ securityColumns.map(col => {
1774
+ const key = 'accessorKey' in col ? col.accessorKey : undefined;
1775
+ if (key === 'objectName') {
1776
+ return {
1777
+ ...col,
1778
+ meta: {
1779
+ ...col.meta,
1780
+ description: { type: 'text' as const, content: 'Target resource' },
1781
+ },
1782
+ };
1783
+ }
1784
+ if (key === 'sourceIp') {
1785
+ return {
1786
+ ...col,
1787
+ meta: {
1788
+ ...col.meta,
1789
+ description: { type: 'tooltip' as const, content: 'Request origin IP' },
1790
+ },
1791
+ };
1792
+ }
1793
+ if (key === 'requests') {
1794
+ return {
1795
+ ...col,
1796
+ meta: { ...col.meta, description: { type: 'tooltip' as const, content: 'Total hits' } },
1797
+ };
1798
+ }
1799
+ if (key === 'parameter') {
1800
+ return {
1801
+ ...col,
1802
+ meta: {
1803
+ ...col.meta,
1804
+ description: { type: 'text' as const, content: 'Affected param' },
1805
+ },
1806
+ };
1807
+ }
1808
+ return col;
1809
+ }),
1810
+ [],
1811
+ );
1812
+
1813
+ return (
1814
+ <Table
1815
+ data={securityEvents}
1816
+ columns={columns}
1817
+ getRowId={row => row.id}
1818
+ sorting={sorting}
1819
+ onSortingChange={setSorting}
1820
+ />
1821
+ );
1822
+ };
1823
+
1824
+ /**
1825
+ * The first column is the master cell — pinned, and the place per-row actions appear on hover,
1826
+ * so every row keeps its verbs in one predictable spot.
1827
+ */
1828
+ export const MasterCellWithActions: StoryFn<typeof meta> = () => {
1829
+ const [sorting, setSorting] = useState<TableSortingState>([]);
1830
+ const [columnSizing, setColumnSizing] = useState<TableColumnSizingState>({});
1831
+
1832
+ const data = useMemo(() => multiplySecurityEvents(), []);
1833
+
1834
+ const columns = useMemo<TableColumnDef<SecurityEvent>[]>(
1835
+ () =>
1836
+ securityColumns.map((col, i) =>
1837
+ i === 0
1838
+ ? {
1839
+ ...col,
1840
+ meta: {
1841
+ ...col.meta,
1842
+ size: 400,
1843
+ resizeType: 'resize',
1844
+ renderMenuAction: (row: { original: SecurityEvent }) => (
1845
+ <DropdownMenu>
1846
+ <DropdownMenuTrigger asChild>
1847
+ <Button variant='ghost' color='neutral' size='small' aria-label='More'>
1848
+ <Ellipsis />
1849
+ </Button>
1850
+ </DropdownMenuTrigger>
1851
+ <DropdownMenuContent>
1852
+ <DropdownMenuItem
1853
+ onSelect={() => navigator.clipboard.writeText(row.original.objectName)}
1854
+ >
1855
+ <DropdownMenuItemIcon>
1856
+ <Copy />
1857
+ </DropdownMenuItemIcon>
1858
+ <DropdownMenuItemText>Copy name</DropdownMenuItemText>
1859
+ </DropdownMenuItem>
1860
+ <DropdownMenuSeparator />
1861
+ <DropdownMenuItem onSelect={() => onShowOnly(row.original.objectName)}>
1862
+ <DropdownMenuItemIcon>
1863
+ <Filter />
1864
+ </DropdownMenuItemIcon>
1865
+ <DropdownMenuItemText>Show only</DropdownMenuItemText>
1866
+ </DropdownMenuItem>
1867
+ <DropdownMenuItem onSelect={() => onExclude(row.original.objectName)}>
1868
+ <DropdownMenuItemIcon>
1869
+ <FilterX />
1870
+ </DropdownMenuItemIcon>
1871
+ <DropdownMenuItemText>Exclude</DropdownMenuItemText>
1872
+ </DropdownMenuItem>
1873
+ </DropdownMenuContent>
1874
+ </DropdownMenu>
1875
+ ),
1876
+ },
1877
+ }
1878
+ : col,
1879
+ ),
1880
+ [],
1881
+ );
1882
+
1883
+ return (
1884
+ <Table
1885
+ className='max-w-920'
1886
+ data={data}
1887
+ columns={columns}
1888
+ getRowId={row => row.id}
1889
+ sorting={sorting}
1890
+ onSortingChange={setSorting}
1891
+ columnSizing={columnSizing}
1892
+ onColumnSizingChange={setColumnSizing}
1893
+ />
1894
+ );
1895
+ };
1896
+
1897
+ /**
1898
+ * `onMasterCellClick` with `activeRowId`: the master cell opens a `Drawer` and the row it
1899
+ * belongs to stays marked while the panel is open.
1900
+ */
1901
+ export const MasterCellWithDrawer: StoryFn<typeof meta> = () => {
1902
+ const [sorting, setSorting] = useState<TableSortingState>([]);
1903
+ const [activeRowId, setActiveRowId] = useState<string | null>(null);
1904
+ // The last opened row stays rendered while the drawer animates closed.
1905
+ const [shownRowId, setShownRowId] = useState<string | null>(null);
1906
+
1907
+ const data = useMemo(
1908
+ () =>
1909
+ Array.from({ length: 4 }, (_, batch) =>
1910
+ securityEvents.map(row => ({
1911
+ ...row,
1912
+ id: `${row.id}-${batch}`,
1913
+ objectName: batch === 0 ? row.objectName : `${row.objectName} (${batch + 1})`,
1914
+ })),
1915
+ ).flat(),
1916
+ [],
1917
+ );
1918
+
1919
+ const columns = useMemo<TableColumnDef<SecurityEvent>[]>(
1920
+ () =>
1921
+ securityColumns.map((col, i) =>
1922
+ i === 0
1923
+ ? {
1924
+ ...col,
1925
+ cell: ({ row }: { row: { original: SecurityEvent } }) => (
1926
+ <Text size='xs'>{row.original.objectName}</Text>
1927
+ ),
1928
+ }
1929
+ : col,
1930
+ ),
1931
+ [],
1932
+ );
1933
+
1934
+ const handleMasterCellClick = useCallback((rowId: string) => {
1935
+ setActiveRowId(prev => (prev === rowId ? null : rowId));
1936
+ setShownRowId(rowId);
1937
+ }, []);
1938
+
1939
+ const handleOpenChange = (open: boolean) => {
1940
+ if (!open) setActiveRowId(null);
1941
+ };
1942
+
1943
+ const shownRow = data.find(d => d.id === shownRowId);
1944
+
1945
+ return (
1946
+ <>
1947
+ <Table
1948
+ className='max-w-920'
1949
+ data={data}
1950
+ columns={columns}
1951
+ getRowId={row => row.id}
1952
+ sorting={sorting}
1953
+ onSortingChange={setSorting}
1954
+ onMasterCellClick={handleMasterCellClick}
1955
+ activeRowId={activeRowId}
1956
+ />
1957
+ <Drawer
1958
+ open={activeRowId !== null}
1959
+ onOpenChange={handleOpenChange}
1960
+ modal={false}
1961
+ overlay={false}
1962
+ closeOnOutsideClick={false}
1963
+ width={640}
1964
+ >
1965
+ <DrawerContent>
1966
+ <DrawerHeader>
1967
+ <DrawerTitle>{shownRow?.objectName}</DrawerTitle>
1968
+ </DrawerHeader>
1969
+ <DrawerBody>
1970
+ <VStack gap={6}>
1971
+ <Attribute orientation='horizontal'>
1972
+ <AttributeLabel width={140}>Status</AttributeLabel>
1973
+ <AttributeValue>
1974
+ <Badge
1975
+ variant='dotted'
1976
+ type='secondary'
1977
+ color={shownRow?.status === 'Blocked' ? 'red' : 'yellow'}
1978
+ >
1979
+ {shownRow?.status}
1980
+ </Badge>
1981
+ </AttributeValue>
1982
+ </Attribute>
1983
+ <Attribute orientation='horizontal'>
1984
+ <AttributeLabel width={140}>Requests</AttributeLabel>
1985
+ <AttributeValue>{shownRow?.requests.toLocaleString()}</AttributeValue>
1986
+ </Attribute>
1987
+ <Attribute orientation='horizontal'>
1988
+ <AttributeLabel width={140}>First detected</AttributeLabel>
1989
+ <AttributeValue>{shownRow?.firstDetected}</AttributeValue>
1990
+ </Attribute>
1991
+ </VStack>
1992
+ </DrawerBody>
1993
+ </DrawerContent>
1994
+ </Drawer>
1995
+ </>
1996
+ );
1997
+ };
1998
+
1999
+ /**
2000
+ * Master cell with both an action menu and a drawer:
2001
+ * clicking the cell body opens the drawer, clicking the action button opens the menu
2002
+ * without triggering the drawer.
2003
+ */
2004
+ export const MasterCellWithActionsAndDrawer: StoryFn<typeof meta> = () => {
2005
+ const [sorting, setSorting] = useState<TableSortingState>([]);
2006
+ const [activeRowId, setActiveRowId] = useState<string | null>(null);
2007
+ // The last opened row stays rendered while the drawer animates closed.
2008
+ const [shownRowId, setShownRowId] = useState<string | null>(null);
2009
+
2010
+ const data = useMemo(() => multiplySecurityEvents(), []);
2011
+
2012
+ const columns = useMemo<TableColumnDef<SecurityEvent>[]>(
2013
+ () =>
2014
+ securityColumns.map((col, i) =>
2015
+ i === 0
2016
+ ? {
2017
+ ...col,
2018
+ cell: ({ row }: { row: { original: SecurityEvent } }) => (
2019
+ <Text size='xs'>{row.original.objectName}</Text>
2020
+ ),
2021
+ meta: {
2022
+ ...col.meta,
2023
+ resizeType: 'resize',
2024
+ renderMenuAction: (row: { original: SecurityEvent }) => (
2025
+ <DropdownMenu>
2026
+ <DropdownMenuTrigger asChild>
2027
+ <Button variant='ghost' color='neutral' size='small' aria-label='More'>
2028
+ <Ellipsis />
2029
+ </Button>
2030
+ </DropdownMenuTrigger>
2031
+ <DropdownMenuContent>
2032
+ <DropdownMenuItem
2033
+ onSelect={() => navigator.clipboard.writeText(row.original.objectName)}
2034
+ >
2035
+ <DropdownMenuItemIcon>
2036
+ <Copy />
2037
+ </DropdownMenuItemIcon>
2038
+ <DropdownMenuItemText>Copy name</DropdownMenuItemText>
2039
+ </DropdownMenuItem>
2040
+ <DropdownMenuSeparator />
2041
+ <DropdownMenuItem onSelect={() => onShowOnly(row.original.objectName)}>
2042
+ <DropdownMenuItemIcon>
2043
+ <Filter />
2044
+ </DropdownMenuItemIcon>
2045
+ <DropdownMenuItemText>Show only</DropdownMenuItemText>
2046
+ </DropdownMenuItem>
2047
+ <DropdownMenuItem onSelect={() => onExclude(row.original.objectName)}>
2048
+ <DropdownMenuItemIcon>
2049
+ <FilterX />
2050
+ </DropdownMenuItemIcon>
2051
+ <DropdownMenuItemText>Exclude</DropdownMenuItemText>
2052
+ </DropdownMenuItem>
2053
+ </DropdownMenuContent>
2054
+ </DropdownMenu>
2055
+ ),
2056
+ },
2057
+ }
2058
+ : col,
2059
+ ),
2060
+ [],
2061
+ );
2062
+
2063
+ const handleMasterCellClick = useCallback((rowId: string) => {
2064
+ setActiveRowId(prev => (prev === rowId ? null : rowId));
2065
+ setShownRowId(rowId);
2066
+ }, []);
2067
+
2068
+ const handleOpenChange = (open: boolean) => {
2069
+ if (!open) setActiveRowId(null);
2070
+ };
2071
+
2072
+ const shownRow = data.find(d => d.id === shownRowId);
2073
+
2074
+ return (
2075
+ <>
2076
+ <Table
2077
+ className='max-w-920'
2078
+ data={data}
2079
+ columns={columns}
2080
+ getRowId={row => row.id}
2081
+ sorting={sorting}
2082
+ onSortingChange={setSorting}
2083
+ onMasterCellClick={handleMasterCellClick}
2084
+ activeRowId={activeRowId}
2085
+ />
2086
+ <Drawer
2087
+ open={activeRowId !== null}
2088
+ onOpenChange={handleOpenChange}
2089
+ modal={false}
2090
+ overlay={false}
2091
+ closeOnOutsideClick={false}
2092
+ width={960}
2093
+ >
2094
+ <DrawerContent>
2095
+ {shownRow && renderSecurityPreviewHeader({ original: shownRow })}
2096
+ <DrawerBody>
2097
+ {shownRow && renderSecurityPreviewContent({ original: shownRow })}
2098
+ </DrawerBody>
2099
+ </DrawerContent>
2100
+ </Drawer>
2101
+ </>
2102
+ );
2103
+ };
2104
+
2105
+ /**
2106
+ * Grouping, expansion, selection, pinning, virtualization and the action bar in one table —
2107
+ * the honest test of whether the features cooperate rather than merely exist.
2108
+ */
2109
+ export const FullFeatured: StoryFn<typeof meta> = () => {
2110
+ const data = useMemo(() => createLargeGroupedData(12, 50), []);
2111
+
2112
+ const [sorting, setSorting] = useState<TableSortingState>([{ id: 'lastEdited', desc: true }]);
2113
+ const [rowSelection, setRowSelection] = useState<TableRowSelectionState>({});
2114
+ const [columnSizing, setColumnSizing] = useState<TableColumnSizingState>({});
2115
+ const [columnPinning, setColumnPinning] = useState<TableColumnPinningState>({
2116
+ left: ['objectName'],
2117
+ });
2118
+ const [columnOrder, setColumnOrder] = useState<string[]>([]);
2119
+ const [columnVisibility, setColumnVisibility] = useState<TableVisibilityState>({});
2120
+ const [expanded, setExpanded] = useState<TableExpandedState>({
2121
+ 'group-0': true,
2122
+ 'group-1': true,
2123
+ });
2124
+
2125
+ return (
2126
+ <Table<SecurityHeaderEntry>
2127
+ className='h-500'
2128
+ data={data}
2129
+ columns={fullFeaturedColumns}
2130
+ getRowId={row => row.id}
2131
+ getSubRows={row => row.children}
2132
+ // Grouping
2133
+ expanded={expanded}
2134
+ onExpandedChange={setExpanded}
2135
+ // Sorting
2136
+ sorting={sorting}
2137
+ onSortingChange={setSorting}
2138
+ // Row selection
2139
+ rowSelection={rowSelection}
2140
+ onRowSelectionChange={setRowSelection}
2141
+ // Column resizing
2142
+ columnSizing={columnSizing}
2143
+ onColumnSizingChange={setColumnSizing}
2144
+ // Column pinning
2145
+ columnPinning={columnPinning}
2146
+ onColumnPinningChange={setColumnPinning}
2147
+ // Column DnD ordering
2148
+ columnOrder={columnOrder}
2149
+ onColumnOrderChange={setColumnOrder}
2150
+ // Column visibility + settings
2151
+ columnVisibility={columnVisibility}
2152
+ onColumnVisibilityChange={setColumnVisibility}
2153
+ defaultColumnVisibility={{}}
2154
+ defaultColumnOrder={headerColumnIds}
2155
+ // Virtual scrolling
2156
+ virtualized='container'
2157
+ >
2158
+ <TableActionBar>
2159
+ <Button variant='ghost' color='neutral-alt' onClick={onDuplicate}>
2160
+ <Copy /> Duplicate
2161
+ </Button>
2162
+ <Button color='destructive' onClick={onDelete}>
2163
+ <Trash2 /> Delete
2164
+ </Button>
2165
+ </TableActionBar>
2166
+ </Table>
2167
+ );
2168
+ };
2169
+ FullFeatured.tags = ['!autodocs'];
2170
+
2171
+ // ---------------------------------------------------------------------------
2172
+ // Inline cell editing (WDS-157)
2173
+ // ---------------------------------------------------------------------------
2174
+ // Table has no dedicated "editable cell" API — a column's `cell` renderer may
2175
+ // return any React node. Here that node is the purpose-built `EditableCell`
2176
+ // (see ./EditableCell), which owns the full body-cell box so nothing shifts
2177
+ // across idle / hover / editing: the whole cell highlights grey on hover and
2178
+ // draws a brand-orange border while editing (text) or open (select). The text
2179
+ // editor swaps in a borderless input in place; the select cell IS the trigger,
2180
+ // so its read view never gets replaced.
2181
+
2182
+ const statusItems: SelectDataItem[] = [
2183
+ { label: 'Blocked', value: 'Blocked' },
2184
+ { label: 'Monitoring', value: 'Monitoring' },
2185
+ ];
2186
+
2187
+ // Empty-by-default select — the placeholder text is supplied by the consumer.
2188
+ const categoryItems: SelectDataItem[] = [
2189
+ { label: 'Category A', value: 'a' },
2190
+ { label: 'Category B', value: 'b' },
2191
+ { label: 'Category C', value: 'c' },
2192
+ { label: 'Category D', value: 'd' },
2193
+ ];
2194
+
2195
+ const StatusBadge: FC<{ value: SecurityEvent['status'] }> = ({ value }) => (
2196
+ <Badge
2197
+ variant='dotted'
2198
+ color={value === 'Blocked' ? 'red' : 'yellow'}
2199
+ type='secondary'
2200
+ size='medium'
2201
+ >
2202
+ {value}
2203
+ </Badge>
2204
+ );
2205
+
2206
+ /**
2207
+ * `EditableTextCell` and `EditableSelectCell` in the plain columns, with the master column
2208
+ * deliberately read-only — editing in place suits a value the user owns, not one the system
2209
+ * reports. Hovering highlights the whole cell, clicking opens it without the content shifting,
2210
+ * and Enter or blur commits while Escape reverts.
2211
+ */
2212
+ /**
2213
+ * Drag-handle column appears when `onRowReorder` is provided. The consumer uses `arrayMove`
2214
+ * (or equivalent) in the callback to reorder its data array.
2215
+ */
2216
+ export const RowReordering: StoryFn<typeof meta> = () => {
2217
+ const [data, setData] = useState(() => securityEvents.slice(0, 6));
2218
+
2219
+ const handleRowReorder = useCallback((event: TableRowReorderEvent) => {
2220
+ setData(prev => {
2221
+ const oldIndex = prev.findIndex(row => row.id === event.activeRowId);
2222
+ const newIndex = prev.findIndex(row => row.id === event.overRowId);
2223
+ if (oldIndex === -1 || newIndex === -1) return prev;
2224
+ return arrayMove(prev, oldIndex, newIndex);
2225
+ });
2226
+ }, []);
2227
+
2228
+ return (
2229
+ <Table
2230
+ data={data}
2231
+ columns={securityColumns}
2232
+ getRowId={row => row.id}
2233
+ onRowReorder={handleRowReorder}
2234
+ data-testid='row-reorder-table'
2235
+ />
2236
+ );
2237
+ };
2238
+
2239
+ /**
2240
+ * With `getSubRows` grouping, a row moves only among the rows of its own group; group rows stay
2241
+ * put. `showEmptyGroups` keeps a group whose `getSubRows` returns `[]` on screen as a group row.
2242
+ */
2243
+ export const RowReorderingInGroups: StoryFn<typeof meta> = () => {
2244
+ const [data, setData] = useState<SecurityHeaderEntry[]>(() => [
2245
+ ...groupedHeaderData,
2246
+ { ...groupedHeaderData[0]!, id: 'group-empty', objectName: 'Nothing here yet', children: [] },
2247
+ ]);
2248
+ const [expanded, setExpanded] = useState<TableExpandedState>(true);
2249
+
2250
+ const handleRowReorder = useCallback((event: TableRowReorderEvent) => {
2251
+ setData(prev =>
2252
+ prev.map(group => {
2253
+ const children = group.children ?? [];
2254
+ const oldIndex = children.findIndex(row => row.id === event.activeRowId);
2255
+ const newIndex = children.findIndex(row => row.id === event.overRowId);
2256
+ if (oldIndex === -1 || newIndex === -1) return group;
2257
+ return { ...group, children: arrayMove(children, oldIndex, newIndex) };
2258
+ }),
2259
+ );
2260
+ }, []);
2261
+
2262
+ return (
2263
+ <Table<SecurityHeaderEntry>
2264
+ data={data}
2265
+ columns={headerColumns}
2266
+ getRowId={row => row.id}
2267
+ getSubRows={row => row.children}
2268
+ showEmptyGroups
2269
+ expanded={expanded}
2270
+ onExpandedChange={setExpanded}
2271
+ onRowReorder={handleRowReorder}
2272
+ data-testid='row-reorder-groups-table'
2273
+ />
2274
+ );
2275
+ };
2276
+
2277
+ const BANNER_ROW_PREFIX = 'banner-';
2278
+ const LOCKED_ROW_IDS = new Set(['csp-1']);
2279
+
2280
+ /**
2281
+ * `groupRowSpan: 'full'` draws `renderGroupRow` across every data column — title on the left,
2282
+ * actions on the right edge; the content owns the collapse toggle. `renderFullWidthRow` turns a
2283
+ * regular row into a single spanning banner, and `getRowCanDrag` keeps the banner and the locked
2284
+ * first header in place while the other rows of the group reorder around them.
2285
+ */
2286
+ export const GroupedWithFullWidthRows: StoryFn<typeof meta> = () => {
2287
+ const [data, setData] = useState<SecurityHeaderEntry[]>(() => [
2288
+ {
2289
+ ...groupedHeaderData[0]!,
2290
+ children: [
2291
+ { ...groupedHeaderData[0]!, id: `${BANNER_ROW_PREFIX}csp`, children: undefined },
2292
+ ...(groupedHeaderData[0]!.children ?? []),
2293
+ ],
2294
+ },
2295
+ ...groupedHeaderData.slice(1, 3),
2296
+ { ...groupedHeaderData[0]!, id: 'group-empty', objectName: 'Nothing here yet', children: [] },
2297
+ ]);
2298
+ const [expanded, setExpanded] = useState<TableExpandedState>(true);
2299
+
2300
+ const handleRowReorder = useCallback((event: TableRowReorderEvent) => {
2301
+ setData(prev =>
2302
+ prev.map(group => {
2303
+ const children = group.children ?? [];
2304
+ const oldIndex = children.findIndex(row => row.id === event.activeRowId);
2305
+ const newIndex = children.findIndex(row => row.id === event.overRowId);
2306
+ if (oldIndex === -1 || newIndex === -1) return group;
2307
+ return { ...group, children: arrayMove(children, oldIndex, newIndex) };
2308
+ }),
2309
+ );
2310
+ }, []);
2311
+
2312
+ return (
2313
+ <Table<SecurityHeaderEntry>
2314
+ data={data}
2315
+ columns={headerColumns}
2316
+ getRowId={row => row.id}
2317
+ getSubRows={row => row.children}
2318
+ showEmptyGroups
2319
+ expanded={expanded}
2320
+ onExpandedChange={setExpanded}
2321
+ onRowReorder={handleRowReorder}
2322
+ groupRowSpan='full'
2323
+ renderGroupRow={row => {
2324
+ const isExpanded = row.getIsExpanded();
2325
+ return (
2326
+ <HStack gap={8} align='center'>
2327
+ <Button
2328
+ variant='ghost'
2329
+ color='neutral'
2330
+ size='small'
2331
+ aria-expanded={isExpanded}
2332
+ aria-label={isExpanded ? 'Collapse group' : 'Expand group'}
2333
+ onClick={row.getToggleExpandedHandler()}
2334
+ >
2335
+ {isExpanded ? <ChevronDown /> : <ChevronRight />}
2336
+ </Button>
2337
+ <Text weight='medium'>{row.original.objectName}</Text>
2338
+ <Badge color='gray' type='secondary' size='medium'>
2339
+ {row.subRows.filter(r => !r.id.startsWith(BANNER_ROW_PREFIX)).length}
2340
+ </Badge>
2341
+ <HStack gap={4} className='ml-auto'>
2342
+ <Button variant='ghost' color='neutral' size='small'>
2343
+ <Plus />
2344
+ Add header
2345
+ </Button>
2346
+ <Button variant='ghost' color='neutral' size='small' aria-label='More actions'>
2347
+ <Ellipsis />
2348
+ </Button>
2349
+ </HStack>
2350
+ </HStack>
2351
+ );
2352
+ }}
2353
+ renderFullWidthRow={row =>
2354
+ row.id.startsWith(BANNER_ROW_PREFIX) ? (
2355
+ <HStack gap={8} align='center'>
2356
+ <Shield size='sm' />
2357
+ <Text size='sm' weight='medium'>
2358
+ Wallarm baseline headers
2359
+ </Text>
2360
+ <Text size='sm' color='secondary'>
2361
+ · applied first · built-in, always on
2362
+ </Text>
2363
+ <Button variant='ghost' color='neutral' size='small' className='ml-auto'>
2364
+ Manage baseline →
2365
+ </Button>
2366
+ </HStack>
2367
+ ) : undefined
2368
+ }
2369
+ getRowCanDrag={row => !row.id.startsWith(BANNER_ROW_PREFIX) && !LOCKED_ROW_IDS.has(row.id)}
2370
+ data-testid='grouped-full-width-table'
2371
+ />
2372
+ );
2373
+ };
2374
+
2375
+ /**
2376
+ * `groupExpandColumn` keeps the expand column in grouping mode: the toggle sits in its own column,
2377
+ * first, and every row of a group — drag handle included — sits to the right of it, while
2378
+ * `renderGroupRow` keeps the title and the actions.
2379
+ */
2380
+ export const GroupedWithExpandColumn: StoryFn<typeof meta> = () => {
2381
+ const [data, setData] = useState<SecurityHeaderEntry[]>(() => [
2382
+ ...groupedHeaderData,
2383
+ { ...groupedHeaderData[0]!, id: 'group-empty', objectName: 'Nothing here yet', children: [] },
2384
+ ]);
2385
+ const [expanded, setExpanded] = useState<TableExpandedState>(true);
2386
+
2387
+ const handleRowReorder = useCallback((event: TableRowReorderEvent) => {
2388
+ setData(prev =>
2389
+ prev.map(group => {
2390
+ const children = group.children ?? [];
2391
+ const oldIndex = children.findIndex(row => row.id === event.activeRowId);
2392
+ const newIndex = children.findIndex(row => row.id === event.overRowId);
2393
+ if (oldIndex === -1 || newIndex === -1) return group;
2394
+ return { ...group, children: arrayMove(children, oldIndex, newIndex) };
2395
+ }),
2396
+ );
2397
+ }, []);
2398
+
2399
+ return (
2400
+ <Table<SecurityHeaderEntry>
2401
+ data={data}
2402
+ columns={headerColumns}
2403
+ getRowId={row => row.id}
2404
+ getSubRows={row => row.children}
2405
+ showEmptyGroups
2406
+ expanded={expanded}
2407
+ onExpandedChange={setExpanded}
2408
+ onRowReorder={handleRowReorder}
2409
+ groupRowSpan='full'
2410
+ groupExpandColumn
2411
+ renderGroupRow={row => (
2412
+ <HStack gap={8} align='center'>
2413
+ <Text weight='medium'>{row.original.objectName}</Text>
2414
+ <Badge color='gray' type='secondary' size='medium'>
2415
+ {row.subRows.length}
2416
+ </Badge>
2417
+ <Button variant='ghost' color='neutral' size='small' className='ml-auto'>
2418
+ <Plus />
2419
+ Add header
2420
+ </Button>
2421
+ </HStack>
2422
+ )}
2423
+ data-testid='grouped-expand-column-table'
2424
+ />
2425
+ );
2426
+ };
2427
+
2428
+ /**
2429
+ * Row reordering coexists with row selection — each feature has its own column and they
2430
+ * don't interfere with each other.
2431
+ */
2432
+ export const RowReorderingWithSelection: StoryFn<typeof meta> = () => {
2433
+ const [data, setData] = useState(() => securityEvents.slice(0, 6));
2434
+ const [rowSelection, setRowSelection] = useState<TableRowSelectionState>({});
2435
+
2436
+ const handleRowReorder = useCallback((event: TableRowReorderEvent) => {
2437
+ setData(prev => {
2438
+ const oldIndex = prev.findIndex(row => row.id === event.activeRowId);
2439
+ const newIndex = prev.findIndex(row => row.id === event.overRowId);
2440
+ if (oldIndex === -1 || newIndex === -1) return prev;
2441
+ return arrayMove(prev, oldIndex, newIndex);
2442
+ });
2443
+ }, []);
2444
+
2445
+ return (
2446
+ <Table
2447
+ data={data}
2448
+ columns={securityColumns}
2449
+ getRowId={row => row.id}
2450
+ onRowReorder={handleRowReorder}
2451
+ rowSelection={rowSelection}
2452
+ onRowSelectionChange={setRowSelection}
2453
+ />
2454
+ );
2455
+ };
2456
+
2457
+ /**
2458
+ * Row drag-and-drop with container virtualization — the `DragOverlay` renders the dragged row
2459
+ * outside the virtualizer so it stays visible even when scrolled out of the virtual window.
2460
+ */
2461
+ export const RowReorderingVirtualized: StoryFn<typeof meta> = () => {
2462
+ const [data, setData] = useState(() => createLargeSecurityEvents(100));
2463
+
2464
+ const handleRowReorder = useCallback((event: TableRowReorderEvent) => {
2465
+ setData(prev => {
2466
+ const oldIndex = prev.findIndex(row => row.id === event.activeRowId);
2467
+ const newIndex = prev.findIndex(row => row.id === event.overRowId);
2468
+ if (oldIndex === -1 || newIndex === -1) return prev;
2469
+ return arrayMove(prev, oldIndex, newIndex);
2470
+ });
2471
+ }, []);
2472
+
2473
+ return (
2474
+ <Table
2475
+ data={data}
2476
+ columns={securityColumns}
2477
+ getRowId={row => row.id}
2478
+ onRowReorder={handleRowReorder}
2479
+ virtualized='container'
2480
+ estimateRowHeight={() => 40}
2481
+ />
2482
+ );
2483
+ };
2484
+ RowReorderingVirtualized.tags = ['!autodocs'];
2485
+
2486
+ export const InlineCellEditing: StoryFn<typeof meta> = () => {
2487
+ const [data, setData] = useState<SecurityEvent[]>(() => securityEvents.slice(0, 6));
2488
+ // Category starts unselected for every row, so the select shows its placeholder.
2489
+ const [categories, setCategories] = useState<Record<string, string>>({});
2490
+
2491
+ const updateCell = useCallback(
2492
+ <K extends keyof SecurityEvent>(rowId: string, key: K, value: SecurityEvent[K]) => {
2493
+ setData(prev => prev.map(row => (row.id === rowId ? { ...row, [key]: value } : row)));
2494
+ },
2495
+ [],
2496
+ );
2497
+
2498
+ const columns = useMemo<TableColumnDef<SecurityEvent>[]>(
2499
+ () => [
2500
+ // The first column is the Table's pinned "master" column — keep it
2501
+ // read-only; editable cells live in the plain columns after it.
2502
+ securityColumnHelper.accessor('sourceIp', {
2503
+ header: 'Source',
2504
+ size: 200,
2505
+ meta: { resizeType: 'cut' as const },
2506
+ cell: ({ getValue }) => <Text size='sm'>{getValue()}</Text>,
2507
+ }),
2508
+ securityColumnHelper.accessor('objectName', {
2509
+ header: 'Object name',
2510
+ size: 400,
2511
+ meta: EDITABLE_CELL_COLUMN_META,
2512
+ cell: ({ row, getValue }) => (
2513
+ <EditableTextCell
2514
+ data-testid='editable-text-cell'
2515
+ value={getValue()}
2516
+ aria-label='Edit object name'
2517
+ onCommit={value => updateCell(row.id, 'objectName', value)}
2518
+ />
2519
+ ),
2520
+ }),
2521
+ securityColumnHelper.accessor('status', {
2522
+ header: 'Status',
2523
+ size: 200,
2524
+ meta: EDITABLE_CELL_COLUMN_META,
2525
+ cell: ({ row, getValue }) => (
2526
+ <EditableSelectCell
2527
+ data-testid='editable-select-cell'
2528
+ value={getValue()}
2529
+ items={statusItems}
2530
+ onCommit={value => updateCell(row.id, 'status', value as SecurityEvent['status'])}
2531
+ >
2532
+ <StatusBadge value={getValue()} />
2533
+ </EditableSelectCell>
2534
+ ),
2535
+ }),
2536
+ // Empty-by-default select: consumer supplies the `placeholder` text.
2537
+ securityColumnHelper.display({
2538
+ id: 'category',
2539
+ header: 'Category',
2540
+ size: 200,
2541
+ meta: EDITABLE_CELL_COLUMN_META,
2542
+ cell: ({ row }) => {
2543
+ const value = categories[row.id] ?? '';
2544
+ return (
2545
+ <EditableSelectCell
2546
+ value={value}
2547
+ items={categoryItems}
2548
+ placeholder='Select'
2549
+ aria-label='Edit category'
2550
+ onCommit={next => setCategories(prev => ({ ...prev, [row.id]: next }))}
2551
+ >
2552
+ {categoryItems.find(item => item.value === value)?.label}
2553
+ </EditableSelectCell>
2554
+ );
2555
+ },
2556
+ }),
2557
+ securityColumnHelper.accessor('parameter', {
2558
+ header: 'Parameters',
2559
+ size: 240,
2560
+ meta: { resizeType: 'cut' as const },
2561
+ cell: ({ getValue }) => <InlineCodeSnippet code={getValue()} size='sm' copyable={false} />,
2562
+ }),
2563
+ ],
2564
+ [updateCell, categories],
2565
+ );
2566
+
2567
+ return (
2568
+ <VStack gap={12} align='stretch'>
2569
+ <span className='sb-annotation'>click a cell to edit it</span>
2570
+ <Table data={data} columns={columns} getRowId={row => row.id} />
2571
+ </VStack>
2572
+ );
2573
+ };
2574
+
2575
+ /**
2576
+ * The group parent row sticks below the header while its children scroll — scroll through
2577
+ * a group to see the parent pin in place, then release when the group boundary passes.
2578
+ */
2579
+ export const StickyGroupParentStory: StoryFn<typeof meta> = () => {
2580
+ const data = useMemo(() => createLargeGroupedData(4, 20), []);
2581
+ const [expanded, setExpanded] = useState<TableExpandedState>({
2582
+ 'group-0': true,
2583
+ 'group-1': true,
2584
+ 'group-2': true,
2585
+ 'group-3': true,
2586
+ });
2587
+ const [sorting, setSorting] = useState<TableSortingState>([{ id: 'lastEdited', desc: true }]);
2588
+ const [rowSelection, setRowSelection] = useState<TableRowSelectionState>({});
2589
+
2590
+ return (
2591
+ <Table<SecurityHeaderEntry>
2592
+ className='h-300'
2593
+ data={data}
2594
+ columns={headerColumns}
2595
+ getRowId={row => row.id}
2596
+ getSubRows={row => row.children}
2597
+ expanded={expanded}
2598
+ onExpandedChange={setExpanded}
2599
+ sorting={sorting}
2600
+ onSortingChange={setSorting}
2601
+ rowSelection={rowSelection}
2602
+ onRowSelectionChange={setRowSelection}
2603
+ data-testid='sticky-group-parent-table'
2604
+ />
2605
+ );
2606
+ };
2607
+ StickyGroupParentStory.storyName = 'Sticky Group Parent';
2608
+
2609
+ /**
2610
+ * The same sticky-parent behavior under container virtualization with large grouped data —
2611
+ * the overlay stays in the DOM even when the real parent row is unmounted by the virtualizer.
2612
+ */
2613
+ export const StickyGroupParentVirtualized: StoryFn<typeof meta> = () => {
2614
+ const data = useMemo(() => createLargeGroupedData(12, 50), []);
2615
+ const [expanded, setExpanded] = useState<TableExpandedState>({
2616
+ 'group-0': true,
2617
+ 'group-1': true,
2618
+ 'group-2': true,
2619
+ });
2620
+ const [sorting, setSorting] = useState<TableSortingState>([{ id: 'lastEdited', desc: true }]);
2621
+ const [rowSelection, setRowSelection] = useState<TableRowSelectionState>({});
2622
+
2623
+ return (
2624
+ <Table<SecurityHeaderEntry>
2625
+ className='h-500'
2626
+ data={data}
2627
+ columns={headerColumns}
2628
+ getRowId={row => row.id}
2629
+ getSubRows={row => row.children}
2630
+ expanded={expanded}
2631
+ onExpandedChange={setExpanded}
2632
+ sorting={sorting}
2633
+ onSortingChange={setSorting}
2634
+ rowSelection={rowSelection}
2635
+ onRowSelectionChange={setRowSelection}
2636
+ virtualized='container'
2637
+ data-testid='sticky-group-parent-virt-table'
2638
+ />
2639
+ );
2640
+ };
2641
+ StickyGroupParentVirtualized.tags = ['!autodocs'];
2642
+ ```
2643
+
2644
+ ### Table/mocks.tsx
2645
+
2646
+ ```tsx
2647
+ import { useCallback, useMemo, useState } from 'react';
2648
+ import { Check, Copy, Filter, FilterX } from '../../icons';
2649
+ import { abbreviateNumber } from '../../utils/abbreviateNumber';
2650
+ import { Badge } from '../Badge';
2651
+ import { InlineCodeSnippet } from '../CodeSnippet';
2652
+ import type { CountryCode } from '../Country';
2653
+ import { DrawerHeader } from '../Drawer';
2654
+ import {
2655
+ DropdownMenu,
2656
+ DropdownMenuContent,
2657
+ DropdownMenuContextTrigger,
2658
+ DropdownMenuItem,
2659
+ DropdownMenuItemContent,
2660
+ DropdownMenuItemDescription,
2661
+ DropdownMenuItemIcon,
2662
+ DropdownMenuItemText,
2663
+ DropdownMenuSeparator,
2664
+ } from '../DropdownMenu';
2665
+ import { FormatDateTime } from '../FormatDateTime';
2666
+ import { HttpMethod, type HttpMethodName } from '../HttpMethod';
2667
+ import { Ip, IpAddress, IpCountry, IpProvider, type SourceKey, sourceLabels } from '../Ip';
2668
+ import { Link } from '../Link';
2669
+ import {
2670
+ OverflowTooltip,
2671
+ OverflowTooltipContent,
2672
+ OverflowTooltipTrigger,
2673
+ } from '../OverflowTooltip';
2674
+ import { HStack, VStack } from '../Stack';
2675
+ import { Switch, SwitchControl } from '../Switch';
2676
+ import { Text } from '../Text';
2677
+ import { createTableColumnHelper } from './lib';
2678
+ import type { TableColumnDef } from './types';
2679
+
2680
+ // ---------------------------------------------------------------------------
2681
+ // Interfaces
2682
+ // ---------------------------------------------------------------------------
2683
+
2684
+ export interface SecurityEvent {
2685
+ id: string;
2686
+ objectName: string;
2687
+ tags: string[];
2688
+ isActive: boolean;
2689
+ requests: number;
2690
+ sourceIp: string;
2691
+ sourceCountry: CountryCode;
2692
+ sourceProvider: string;
2693
+ parameter: string;
2694
+ status: 'Blocked' | 'Monitoring';
2695
+ firstDetected: string;
2696
+ cweId: string;
2697
+ lastSeen: string;
2698
+ endpointMethod: HttpMethodName;
2699
+ endpointPath: string;
2700
+ state: boolean;
2701
+ }
2702
+
2703
+ export interface SecurityHeaderEntry {
2704
+ id: string;
2705
+ objectName: string;
2706
+ ip: string | null;
2707
+ ipCountry: CountryCode | null;
2708
+ provider: string;
2709
+ requests: number;
2710
+ status: 'New' | 'Active' | 'Resolved';
2711
+ lastEdited: string;
2712
+ lastSynced: string;
2713
+ version: 'Up to date' | 'Outdated';
2714
+ children?: SecurityHeaderEntry[];
2715
+ }
2716
+
2717
+ // ---------------------------------------------------------------------------
2718
+ // Utilities
2719
+ // ---------------------------------------------------------------------------
2720
+
2721
+ // ---------------------------------------------------------------------------
2722
+ // Security Events — flat data
2723
+ // ---------------------------------------------------------------------------
2724
+
2725
+ export const securityEvents: SecurityEvent[] = [
2726
+ {
2727
+ id: '1',
2728
+ objectName: 'Rate limiting abuse on the payment endpoint',
2729
+ tags: ['api-abuse', 'account-takeover', 'credential-stuffing', 'scanner', 'brute-force'],
2730
+ isActive: false,
2731
+ requests: 22000,
2732
+ sourceIp: '34.74.73.20',
2733
+ sourceCountry: 'US',
2734
+ sourceProvider: 'aws',
2735
+ parameter: 'header.X-SESSION-ID',
2736
+ status: 'Blocked',
2737
+ firstDetected: '2026-01-28T15:00:00',
2738
+ cweId: 'CWE-287',
2739
+ lastSeen: '5 days ago',
2740
+ endpointMethod: 'POST',
2741
+ endpointPath: '/api/v1/payments',
2742
+ state: true,
2743
+ },
2744
+ {
2745
+ id: '2',
2746
+ objectName: 'Mass assignment vulnerability in user profile',
2747
+ tags: ['api-abuse', 'account-takeover', 'credential-stuffing', 'scanner', 'brute-force'],
2748
+ isActive: false,
2749
+ requests: 25000,
2750
+ sourceIp: '34.74.73.20',
2751
+ sourceCountry: 'US',
2752
+ sourceProvider: 'aws',
2753
+ parameter: 'header.X-DEVICE-ID',
2754
+ status: 'Blocked',
2755
+ firstDetected: '2026-01-27T16:20:10',
2756
+ cweId: 'CWE-416',
2757
+ lastSeen: '6 days ago',
2758
+ endpointMethod: 'PUT',
2759
+ endpointPath: '/api/v1/users/profile',
2760
+ state: false,
2761
+ },
2762
+ {
2763
+ id: '3',
2764
+ objectName: 'Insecure direct object reference in user data',
2765
+ tags: ['api-abuse', 'account-takeover', 'credential-stuffing', 'scanner', 'brute-force'],
2766
+ isActive: true,
2767
+ requests: 30000,
2768
+ sourceIp: '34.74.73.20',
2769
+ sourceCountry: 'US',
2770
+ sourceProvider: 'aws',
2771
+ parameter: 'header.X-CLIENT-VERSION',
2772
+ status: 'Monitoring',
2773
+ firstDetected: '2026-01-26T17:35:25',
2774
+ cweId: 'CWE-89',
2775
+ lastSeen: '7 days ago',
2776
+ endpointMethod: 'GET',
2777
+ endpointPath: '/api/v1/users/:id',
2778
+ state: true,
2779
+ },
2780
+ {
2781
+ id: '4',
2782
+ objectName: 'Improper error handling leading to info leak',
2783
+ tags: ['api-abuse', 'account-takeover', 'credential-stuffing', 'scanner', 'brute-force'],
2784
+ isActive: true,
2785
+ requests: 35000,
2786
+ sourceIp: '34.74.73.20',
2787
+ sourceCountry: 'US',
2788
+ sourceProvider: 'aws',
2789
+ parameter: 'header.X-ORIGIN',
2790
+ status: 'Blocked',
2791
+ firstDetected: '2026-01-25T18:50:15',
2792
+ cweId: 'CWE-22',
2793
+ lastSeen: '1 week ago',
2794
+ endpointMethod: 'GET',
2795
+ endpointPath: '/api/v1/errors/debug',
2796
+ state: true,
2797
+ },
2798
+ {
2799
+ id: '5',
2800
+ objectName: 'Broken authentication in the login API',
2801
+ tags: ['api-abuse', 'account-takeover', 'credential-stuffing', 'scanner', 'brute-force'],
2802
+ isActive: true,
2803
+ requests: 20000,
2804
+ sourceIp: '34.74.73.20',
2805
+ sourceCountry: 'US',
2806
+ sourceProvider: 'aws',
2807
+ parameter: 'header.X-TIMESTAMP',
2808
+ status: 'Blocked',
2809
+ firstDetected: '2026-01-29T14:45:30',
2810
+ cweId: 'CWE-200',
2811
+ lastSeen: '4 days ago',
2812
+ endpointMethod: 'POST',
2813
+ endpointPath: '/api/v1/auth/login',
2814
+ state: false,
2815
+ },
2816
+ {
2817
+ id: '6',
2818
+ objectName: 'Lack of resource validation in file upload',
2819
+ tags: ['api-abuse', 'account-takeover', 'credential-stuffing', 'scanner', 'brute-force'],
2820
+ isActive: false,
2821
+ requests: 40000,
2822
+ sourceIp: '34.74.73.20',
2823
+ sourceCountry: 'US',
2824
+ sourceProvider: 'aws',
2825
+ parameter: 'header.X-REQUEST-TYPE',
2826
+ status: 'Blocked',
2827
+ firstDetected: '2026-01-24T19:05:45',
2828
+ cweId: 'CWE-352',
2829
+ lastSeen: '2 days ago',
2830
+ endpointMethod: 'POST',
2831
+ endpointPath: '/api/v1/files/upload',
2832
+ state: true,
2833
+ },
2834
+ {
2835
+ id: '7',
2836
+ objectName: 'Server-side request forgery in image proxy',
2837
+ tags: ['api-abuse', 'account-takeover', 'credential-stuffing', 'scanner', 'brute-force'],
2838
+ isActive: false,
2839
+ requests: 50000,
2840
+ sourceIp: '34.74.73.20',
2841
+ sourceCountry: 'US',
2842
+ sourceProvider: 'aws',
2843
+ parameter: 'header.X-CLIENT-ID',
2844
+ status: 'Monitoring',
2845
+ firstDetected: '2026-01-23T20:30:00',
2846
+ cweId: 'CWE-125',
2847
+ lastSeen: '3 days ago',
2848
+ endpointMethod: 'GET',
2849
+ endpointPath: '/api/v1/images/proxy',
2850
+ state: false,
2851
+ },
2852
+ {
2853
+ id: '8',
2854
+ objectName: 'Unvalidated redirects and forwards in auth flow',
2855
+ tags: ['api-abuse', 'account-takeover', 'credential-stuffing', 'scanner', 'brute-force'],
2856
+ isActive: true,
2857
+ requests: 75000,
2858
+ sourceIp: '34.74.73.20',
2859
+ sourceCountry: 'US',
2860
+ sourceProvider: 'aws',
2861
+ parameter: 'header.X-RESPONSE-FORMAT',
2862
+ status: 'Monitoring',
2863
+ firstDetected: '2026-01-22T21:45:50',
2864
+ cweId: 'CWE-134',
2865
+ lastSeen: '4 days ago',
2866
+ endpointMethod: 'GET',
2867
+ endpointPath: '/api/v1/auth/redirect',
2868
+ state: true,
2869
+ },
2870
+ {
2871
+ id: '9',
2872
+ objectName: 'SQL injection in the user ID',
2873
+ tags: ['api-abuse', 'account-takeover', 'credential-stuffing', 'scanner', 'brute-force'],
2874
+ isActive: false,
2875
+ requests: 15000,
2876
+ sourceIp: '34.74.73.20',
2877
+ sourceCountry: 'US',
2878
+ sourceProvider: 'aws',
2879
+ parameter: 'header.X-API-KEY',
2880
+ status: 'Blocked',
2881
+ firstDetected: '2026-01-31T12:15:00',
2882
+ cweId: 'CWE-79',
2883
+ lastSeen: '1 day ago',
2884
+ endpointMethod: 'DELETE',
2885
+ endpointPath: '/api/v1/users/:id',
2886
+ state: false,
2887
+ },
2888
+ {
2889
+ id: '10',
2890
+ objectName: 'Cross-site scripting in the API endpoint',
2891
+ tags: ['api-abuse', 'account-takeover', 'credential-stuffing', 'scanner', 'brute-force'],
2892
+ isActive: false,
2893
+ requests: 18000,
2894
+ sourceIp: '34.74.73.20',
2895
+ sourceCountry: 'US',
2896
+ sourceProvider: 'aws',
2897
+ parameter: 'header.X-USER-ID',
2898
+ status: 'Blocked',
2899
+ firstDetected: '2026-01-30T13:30:45',
2900
+ cweId: 'CWE-89',
2901
+ lastSeen: '3 days ago',
2902
+ endpointMethod: 'PATCH',
2903
+ endpointPath: '/api/v1/content',
2904
+ state: true,
2905
+ },
2906
+ ];
2907
+
2908
+ // ---------------------------------------------------------------------------
2909
+ // Security Events — columns
2910
+ // ---------------------------------------------------------------------------
2911
+
2912
+ export const securityColumnHelper = createTableColumnHelper<SecurityEvent>();
2913
+
2914
+ export const securityColumns: TableColumnDef<SecurityEvent>[] = [
2915
+ securityColumnHelper.accessor('objectName', {
2916
+ header: 'Object name',
2917
+ size: 300,
2918
+ enableSorting: true,
2919
+ meta: { sortType: 'text' as const, resizeType: 'cut' as const },
2920
+ cell: ({ row }) => (
2921
+ <OverflowTooltip>
2922
+ <OverflowTooltipTrigger asChild>
2923
+ <Link size='sm' type='table' weight='regular' href='#'>
2924
+ {row.original.objectName}
2925
+ </Link>
2926
+ </OverflowTooltipTrigger>
2927
+ <OverflowTooltipContent>{row.original.objectName}</OverflowTooltipContent>
2928
+ </OverflowTooltip>
2929
+ ),
2930
+ }),
2931
+ securityColumnHelper.accessor('state', {
2932
+ header: 'State',
2933
+ size: 100,
2934
+ cell: ({ getValue }) => (
2935
+ <Switch checked={getValue()}>
2936
+ <SwitchControl />
2937
+ </Switch>
2938
+ ),
2939
+ }),
2940
+ securityColumnHelper.accessor('requests', {
2941
+ header: 'Requests',
2942
+ size: 160,
2943
+ enableSorting: true,
2944
+ cell: ({ getValue }) => <Text size='sm'>{abbreviateNumber(getValue())}</Text>,
2945
+ }),
2946
+ securityColumnHelper.accessor('sourceIp', {
2947
+ header: 'Source',
2948
+ size: 200,
2949
+ enableSorting: true,
2950
+ meta: { sortType: 'text' as const },
2951
+ cell: ({ row }) => (
2952
+ <Ip>
2953
+ {row.original.sourceCountry && <IpCountry code={row.original.sourceCountry} />}
2954
+ <IpAddress>{row.original.sourceIp}</IpAddress>
2955
+ {row.original.sourceProvider && (
2956
+ <IpProvider>{sourceLabels[row.original.sourceProvider as SourceKey]}</IpProvider>
2957
+ )}
2958
+ </Ip>
2959
+ ),
2960
+ }),
2961
+ securityColumnHelper.accessor('parameter', {
2962
+ header: 'Parameters',
2963
+ size: 220,
2964
+ enableSorting: true,
2965
+ meta: { sortType: 'text' as const, resizeType: 'cut' as const },
2966
+ cell: ({ getValue }) => <InlineCodeSnippet code={getValue()} size='sm' copyable={false} />,
2967
+ }),
2968
+ securityColumnHelper.accessor('status', {
2969
+ header: 'Status',
2970
+ size: 130,
2971
+ enableSorting: true,
2972
+ meta: { sortType: 'text' as const, resizeType: 'cut' as const },
2973
+ cell: ({ getValue }) => {
2974
+ const status = getValue();
2975
+ return (
2976
+ <Badge
2977
+ variant='dotted'
2978
+ color={status === 'Blocked' ? 'red' : 'yellow'}
2979
+ type='secondary'
2980
+ size='medium'
2981
+ >
2982
+ {status}
2983
+ </Badge>
2984
+ );
2985
+ },
2986
+ }),
2987
+ securityColumnHelper.accessor('firstDetected', {
2988
+ header: 'First detected',
2989
+ size: 160,
2990
+ enableSorting: true,
2991
+ meta: { sortType: 'date' as const, resizeType: 'cut' as const },
2992
+ cell: ({ getValue }) => <FormatDateTime value={getValue()} format='relative' />,
2993
+ }),
2994
+ securityColumnHelper.accessor('cweId', {
2995
+ header: 'Security info',
2996
+ size: 130,
2997
+ enableSorting: true,
2998
+ meta: { sortType: 'text' as const },
2999
+ cell: ({ getValue }) => (
3000
+ <Badge color='gray' type='secondary' size='medium' textVariant='code'>
3001
+ {getValue()}
3002
+ </Badge>
3003
+ ),
3004
+ }),
3005
+ securityColumnHelper.accessor('lastSeen', {
3006
+ header: 'Last seen',
3007
+ size: 120,
3008
+ enableSorting: true,
3009
+ meta: { sortType: 'date' as const, resizeType: 'cut' as const },
3010
+ cell: ({ getValue }) => (
3011
+ <Text size='sm' color='secondary'>
3012
+ {getValue()}
3013
+ </Text>
3014
+ ),
3015
+ }),
3016
+ securityColumnHelper.display({
3017
+ id: 'endpoint',
3018
+ header: 'Endpoints',
3019
+ size: 220,
3020
+ cell: ({ row }) => (
3021
+ <HStack gap={6}>
3022
+ <HttpMethod method={row.original.endpointMethod} />
3023
+ <Text size='sm' truncate>
3024
+ {row.original.endpointPath}
3025
+ </Text>
3026
+ </HStack>
3027
+ ),
3028
+ }),
3029
+ ];
3030
+
3031
+ export const securityColumnIds = securityColumns
3032
+ .map(c => ('accessorKey' in c ? (c.accessorKey as string) : (c as { id?: string }).id) as string)
3033
+ .filter(Boolean);
3034
+
3035
+ // ---------------------------------------------------------------------------
3036
+ // Grouped data — hierarchical with children
3037
+ // ---------------------------------------------------------------------------
3038
+
3039
+ const createHeaderEntries = (groupId: string, count: number): SecurityHeaderEntry[] =>
3040
+ Array.from({ length: count }, (_, i) => ({
3041
+ id: `${groupId}-${i + 1}`,
3042
+ objectName: '/v1/antibot/api/report/{parameter_1}',
3043
+ ip: '142.198.167.52/32',
3044
+ ipCountry: 'ES',
3045
+ provider: 'azure',
3046
+ requests: 23000,
3047
+ status: 'New' as const,
3048
+ lastEdited: '2 days ago',
3049
+ lastSynced: '3 months ago',
3050
+ version: 'Up to date' as const,
3051
+ }));
3052
+
3053
+ export const groupedHeaderData: SecurityHeaderEntry[] = [
3054
+ {
3055
+ id: 'group-csp',
3056
+ objectName: 'Content Security Policy',
3057
+ ip: '',
3058
+ ipCountry: null,
3059
+ provider: '',
3060
+ requests: 0,
3061
+ status: 'New',
3062
+ lastEdited: '',
3063
+ lastSynced: '',
3064
+ version: 'Up to date',
3065
+ children: createHeaderEntries('csp', 3),
3066
+ },
3067
+ {
3068
+ id: 'group-cto',
3069
+ objectName: 'Content-Type-Options',
3070
+ ip: '',
3071
+ ipCountry: null,
3072
+ provider: '',
3073
+ requests: 0,
3074
+ status: 'New',
3075
+ lastEdited: '',
3076
+ lastSynced: '',
3077
+ version: 'Up to date',
3078
+ children: createHeaderEntries('cto', 5),
3079
+ },
3080
+ {
3081
+ id: 'group-xfo',
3082
+ objectName: 'X-Frame-Options',
3083
+ ip: '',
3084
+ ipCountry: null,
3085
+ provider: '',
3086
+ requests: 0,
3087
+ status: 'New',
3088
+ lastEdited: '',
3089
+ lastSynced: '',
3090
+ version: 'Up to date',
3091
+ children: createHeaderEntries('xfo', 2),
3092
+ },
3093
+ {
3094
+ id: 'group-hsts',
3095
+ objectName: 'HTTP Strict Transport Security',
3096
+ ip: '',
3097
+ ipCountry: null,
3098
+ provider: '',
3099
+ requests: 0,
3100
+ status: 'New',
3101
+ lastEdited: '',
3102
+ lastSynced: '',
3103
+ version: 'Up to date',
3104
+ children: createHeaderEntries('hsts', 4),
3105
+ },
3106
+ ];
3107
+
3108
+ // ---------------------------------------------------------------------------
3109
+ // Grouped data — columns
3110
+ // ---------------------------------------------------------------------------
3111
+
3112
+ export const headerColumnHelper = createTableColumnHelper<SecurityHeaderEntry>();
3113
+
3114
+ export const headerColumns: TableColumnDef<SecurityHeaderEntry>[] = [
3115
+ headerColumnHelper.accessor('objectName', {
3116
+ header: 'Object name',
3117
+ size: 300,
3118
+ enableSorting: true,
3119
+ meta: { sortType: 'text' as const, resizeType: 'cut' as const },
3120
+ cell: ({ getValue }) => (
3121
+ <Text size='sm' truncate>
3122
+ {getValue()}
3123
+ </Text>
3124
+ ),
3125
+ }),
3126
+ headerColumnHelper.accessor('ip', {
3127
+ header: 'IP',
3128
+ size: 200,
3129
+ enableSorting: true,
3130
+ meta: { sortType: 'text' as const },
3131
+ cell: ({ row }) => (
3132
+ <Ip>
3133
+ {row.original.ipCountry && <IpCountry code={row.original.ipCountry} />}
3134
+ <IpAddress>{row.original.ip}</IpAddress>
3135
+ {row.original.provider && (
3136
+ <IpProvider>{sourceLabels[row.original.provider as SourceKey]}</IpProvider>
3137
+ )}
3138
+ </Ip>
3139
+ ),
3140
+ }),
3141
+ headerColumnHelper.accessor('requests', {
3142
+ header: 'Requests',
3143
+ size: 100,
3144
+ enableSorting: true,
3145
+ cell: ({ getValue }) => <Text size='sm'>{abbreviateNumber(getValue())}</Text>,
3146
+ }),
3147
+ headerColumnHelper.accessor('status', {
3148
+ header: 'Status',
3149
+ size: 100,
3150
+ cell: ({ getValue }) => (
3151
+ <Badge color='blue' type='secondary' size='medium'>
3152
+ {getValue()}
3153
+ </Badge>
3154
+ ),
3155
+ }),
3156
+ headerColumnHelper.accessor('lastEdited', {
3157
+ header: 'Last edited',
3158
+ size: 130,
3159
+ enableSorting: true,
3160
+ meta: { sortType: 'date' as const },
3161
+ cell: ({ getValue }) => (
3162
+ <Text size='sm' color='secondary'>
3163
+ {getValue()}
3164
+ </Text>
3165
+ ),
3166
+ }),
3167
+ headerColumnHelper.accessor('lastSynced', {
3168
+ header: 'Last synced',
3169
+ size: 130,
3170
+ enableSorting: true,
3171
+ meta: { sortType: 'date' as const },
3172
+ cell: ({ getValue }) => (
3173
+ <Text size='sm' color='secondary'>
3174
+ {getValue()}
3175
+ </Text>
3176
+ ),
3177
+ }),
3178
+ headerColumnHelper.display({
3179
+ id: 'version',
3180
+ header: 'Version',
3181
+ size: 140,
3182
+ cell: ({ row }) => (
3183
+ <HStack gap={4} align='center'>
3184
+ <Check size='sm' className='text-text-success' />
3185
+ <Text size='sm' color='inherit'>
3186
+ {row.original.version}
3187
+ </Text>
3188
+ </HStack>
3189
+ ),
3190
+ }),
3191
+ ];
3192
+
3193
+ export const headerColumnIds = headerColumns
3194
+ .map(c => ('accessorKey' in c ? (c.accessorKey as string) : (c as { id?: string }).id) as string)
3195
+ .filter(Boolean);
3196
+
3197
+ // ---------------------------------------------------------------------------
3198
+ // Large grouped data — for FullFeatured + virtualization
3199
+ // ---------------------------------------------------------------------------
3200
+
3201
+ const HEADER_GROUPS = [
3202
+ 'Content Security Policy',
3203
+ 'Content-Type-Options',
3204
+ 'X-Frame-Options',
3205
+ 'HTTP Strict Transport Security',
3206
+ 'Referrer-Policy',
3207
+ 'Permissions-Policy',
3208
+ 'Cross-Origin-Opener-Policy',
3209
+ 'Cross-Origin-Resource-Policy',
3210
+ 'X-Content-Type-Options',
3211
+ 'Cache-Control',
3212
+ 'X-XSS-Protection',
3213
+ 'Expect-CT',
3214
+ ] as const;
3215
+
3216
+ const IPS = ['142.198.167.52/32', '10.0.42.7/24', '192.168.1.100/32', '172.16.0.55/16'] as const;
3217
+ const FLAGS = ['ES', 'US', 'DE', 'JP'] as const;
3218
+ const PROVIDERS = ['azure', 'aws', 'gce', 'aws'] as const;
3219
+ const STATUSES = ['New', 'Active', 'Resolved'] as const;
3220
+ const VERSIONS = ['Up to date', 'Outdated'] as const;
3221
+ const ENDPOINTS = [
3222
+ '/v1/antibot/api/report/{parameter_1}',
3223
+ '/v1/auth/verify/{token}',
3224
+ '/v1/users/{id}/settings',
3225
+ '/v1/webhooks/process/{event_type}',
3226
+ '/v1/analytics/track/{metric}',
3227
+ ] as const;
3228
+
3229
+ export const createLargeGroupedData = (
3230
+ groupCount = 12,
3231
+ childrenPerGroup = 50,
3232
+ ): SecurityHeaderEntry[] => {
3233
+ return Array.from({ length: groupCount }, (_, gi): SecurityHeaderEntry => {
3234
+ const groupName = HEADER_GROUPS.at(gi % HEADER_GROUPS.length)!;
3235
+ const groupId = `group-${gi}`;
3236
+
3237
+ return {
3238
+ id: groupId,
3239
+ objectName: groupName,
3240
+ ip: '',
3241
+ ipCountry: null,
3242
+ provider: '',
3243
+ requests: 0,
3244
+ status: 'New' as const,
3245
+ lastEdited: '',
3246
+ lastSynced: '',
3247
+ version: 'Up to date' as const,
3248
+ children: Array.from({ length: childrenPerGroup }, (_, ci) => ({
3249
+ id: `${groupId}-${ci}`,
3250
+ objectName: ENDPOINTS.at(ci % ENDPOINTS.length)!,
3251
+ ip: IPS.at(ci % IPS.length)!,
3252
+ ipCountry: FLAGS.at(ci % FLAGS.length)!,
3253
+ provider: PROVIDERS.at(ci % PROVIDERS.length)!,
3254
+ requests: 5000 + ci * 1000,
3255
+ status: STATUSES.at(ci % STATUSES.length)!,
3256
+ lastEdited: `${(ci % 14) + 1} days ago`,
3257
+ lastSynced: `${(ci % 6) + 1} months ago`,
3258
+ version: VERSIONS.at(ci % VERSIONS.length)!,
3259
+ })),
3260
+ };
3261
+ });
3262
+ };
3263
+
3264
+ // ---------------------------------------------------------------------------
3265
+ // Large flat data — for a Virtualized story
3266
+ // ---------------------------------------------------------------------------
3267
+
3268
+ /** Shared preview drawer header for security events */
3269
+ export const renderSecurityPreviewHeader = (row: { original: SecurityEvent }) => (
3270
+ <DrawerHeader>
3271
+ <Text size='lg' weight='medium'>
3272
+ {row.original.objectName}
3273
+ </Text>
3274
+ </DrawerHeader>
3275
+ );
3276
+
3277
+ /** Preview drawer content for security events */
3278
+ const SecurityPreviewContent = ({ row }: { row: { original: SecurityEvent } }) => (
3279
+ <VStack gap={16}>
3280
+ <HStack gap={8}>
3281
+ <Badge
3282
+ variant='dotted'
3283
+ color={row.original.status === 'Blocked' ? 'red' : 'yellow'}
3284
+ type='secondary'
3285
+ size='medium'
3286
+ >
3287
+ {row.original.status}
3288
+ </Badge>
3289
+ <Switch checked={row.original.state}>
3290
+ <SwitchControl />
3291
+ </Switch>
3292
+ <InlineCodeSnippet code={row.original.parameter} size='sm' copyable={false} />
3293
+ </HStack>
3294
+ <VStack gap={4}>
3295
+ <Text size='sm' color='secondary'>
3296
+ Source: {row.original.sourceCountry} · {row.original.sourceProvider}
3297
+ </Text>
3298
+ <Text size='sm' color='secondary'>
3299
+ First detected: {row.original.firstDetected}
3300
+ </Text>
3301
+ <Text size='sm' color='secondary'>
3302
+ Last seen: {row.original.lastSeen}
3303
+ </Text>
3304
+ <Text size='sm' color='secondary'>
3305
+ Security: {row.original.cweId}
3306
+ </Text>
3307
+ </VStack>
3308
+ </VStack>
3309
+ );
3310
+
3311
+ SecurityPreviewContent.displayName = 'SecurityPreviewContent';
3312
+
3313
+ /** Preview content (without title — title shown in DrawerHeader) */
3314
+ export const renderSecurityPreviewContent = (row: { original: SecurityEvent }) => (
3315
+ <SecurityPreviewContent row={row} />
3316
+ );
3317
+
3318
+ /** Duplicate securityEvents N times with unique IDs */
3319
+ export const multiplySecurityEvents = (times = 4): SecurityEvent[] =>
3320
+ Array.from({ length: times }, (_, batch) =>
3321
+ securityEvents.map(row => ({
3322
+ ...row,
3323
+ id: `${row.id}-${batch}`,
3324
+ objectName: batch === 0 ? row.objectName : `${row.objectName} (${batch + 1})`,
3325
+ })),
3326
+ ).flat();
3327
+
3328
+ export const createLargeSecurityEvents = (count = 1000): SecurityEvent[] =>
3329
+ Array.from({ length: count }, (_, i) => {
3330
+ const base = securityEvents.at(i % securityEvents.length)!;
3331
+
3332
+ return {
3333
+ ...base,
3334
+ id: String(i + 1),
3335
+ // Deterministic pseudo-random spread so visual snapshots are stable across runs.
3336
+ requests: (i * 2_654_435_761) % 100_000,
3337
+ firstDetected: `2026-01-${String((i % 28) + 1).padStart(2, '0')}T${String(i % 24).padStart(2, '0')}:00:00`,
3338
+ };
3339
+ });
3340
+
3341
+ // ---------------------------------------------------------------------------
3342
+ // FullFeatured — all features combined: grouping, virtual scroll, sorting,
3343
+ // selection, resizing, pinning, DnD ordering, visibility, action bar,
3344
+ // context menu
3345
+ // ---------------------------------------------------------------------------
3346
+
3347
+ export const fullFeaturedColumns: TableColumnDef<SecurityHeaderEntry>[] = headerColumns.map(col => {
3348
+ const key = 'accessorKey' in col ? col.accessorKey : undefined;
3349
+
3350
+ if (key === 'objectName') {
3351
+ return {
3352
+ ...col,
3353
+ size: 220,
3354
+ meta: {
3355
+ ...col.meta,
3356
+ resizeType: 'cut' as const,
3357
+ description: { type: 'tooltip' as const, content: 'Target resource' },
3358
+ },
3359
+ cell: ({ getValue }: { getValue: () => string }) => (
3360
+ <DropdownMenu>
3361
+ <DropdownMenuContextTrigger>
3362
+ <OverflowTooltip>
3363
+ <OverflowTooltipTrigger asChild>
3364
+ <Text size='sm' truncate>
3365
+ {getValue()}
3366
+ </Text>
3367
+ </OverflowTooltipTrigger>
3368
+ <OverflowTooltipContent>{getValue()}</OverflowTooltipContent>
3369
+ </OverflowTooltip>
3370
+ </DropdownMenuContextTrigger>
3371
+ <DropdownMenuContent>
3372
+ <DropdownMenuItem onSelect={() => alert(`Copied: ${getValue()}`)}>
3373
+ <DropdownMenuItemIcon>
3374
+ <Copy />
3375
+ </DropdownMenuItemIcon>
3376
+ <DropdownMenuItemText>Copy value</DropdownMenuItemText>
3377
+ </DropdownMenuItem>
3378
+
3379
+ <DropdownMenuSeparator />
3380
+
3381
+ <DropdownMenuItem onSelect={() => alert(`Show only: ${getValue()}`)}>
3382
+ <DropdownMenuItemIcon>
3383
+ <Filter />
3384
+ </DropdownMenuItemIcon>
3385
+ <DropdownMenuItemContent>
3386
+ <DropdownMenuItemText>Show only</DropdownMenuItemText>
3387
+ <DropdownMenuItemDescription>Filter to matching rows</DropdownMenuItemDescription>
3388
+ </DropdownMenuItemContent>
3389
+ </DropdownMenuItem>
3390
+
3391
+ <DropdownMenuItem onSelect={() => alert(`Exclude: ${getValue()}`)}>
3392
+ <DropdownMenuItemIcon>
3393
+ <FilterX />
3394
+ </DropdownMenuItemIcon>
3395
+ <DropdownMenuItemContent>
3396
+ <DropdownMenuItemText>Exclude</DropdownMenuItemText>
3397
+ <DropdownMenuItemDescription>Filter out matching rows</DropdownMenuItemDescription>
3398
+ </DropdownMenuItemContent>
3399
+ </DropdownMenuItem>
3400
+ </DropdownMenuContent>
3401
+ </DropdownMenu>
3402
+ ),
3403
+ };
3404
+ }
3405
+
3406
+ if (key === 'ip') {
3407
+ return {
3408
+ ...col,
3409
+ meta: { ...col.meta, description: { type: 'tooltip' as const, content: 'Origin IP' } },
3410
+ cell: ({ row }: { row: { original: SecurityHeaderEntry } }) => (
3411
+ <DropdownMenu>
3412
+ <DropdownMenuContextTrigger>
3413
+ <Ip>
3414
+ {row.original.ipCountry && <IpCountry code={row.original.ipCountry} />}
3415
+ <IpAddress>{row.original.ip}</IpAddress>
3416
+ {row.original.provider && (
3417
+ <IpProvider>{sourceLabels[row.original.provider as SourceKey]}</IpProvider>
3418
+ )}
3419
+ </Ip>
3420
+ </DropdownMenuContextTrigger>
3421
+ <DropdownMenuContent>
3422
+ <DropdownMenuItem onSelect={() => alert(`Copied: ${row.original.ip}`)}>
3423
+ <DropdownMenuItemIcon>
3424
+ <Copy />
3425
+ </DropdownMenuItemIcon>
3426
+ <DropdownMenuItemText>Copy IP</DropdownMenuItemText>
3427
+ </DropdownMenuItem>
3428
+
3429
+ <DropdownMenuSeparator />
3430
+
3431
+ <DropdownMenuItem onSelect={() => alert(`Show only: ${row.original.ip}`)}>
3432
+ <DropdownMenuItemIcon>
3433
+ <Filter />
3434
+ </DropdownMenuItemIcon>
3435
+ <DropdownMenuItemContent>
3436
+ <DropdownMenuItemText>Show only</DropdownMenuItemText>
3437
+ <DropdownMenuItemDescription>Filter to this IP</DropdownMenuItemDescription>
3438
+ </DropdownMenuItemContent>
3439
+ </DropdownMenuItem>
3440
+
3441
+ <DropdownMenuItem onSelect={() => alert(`Exclude: ${row.original.ip}`)}>
3442
+ <DropdownMenuItemIcon>
3443
+ <FilterX />
3444
+ </DropdownMenuItemIcon>
3445
+ <DropdownMenuItemContent>
3446
+ <DropdownMenuItemText>Exclude</DropdownMenuItemText>
3447
+ <DropdownMenuItemDescription>Filter out this IP</DropdownMenuItemDescription>
3448
+ </DropdownMenuItemContent>
3449
+ </DropdownMenuItem>
3450
+ </DropdownMenuContent>
3451
+ </DropdownMenu>
3452
+ ),
3453
+ };
3454
+ }
3455
+
3456
+ if (key === 'requests') {
3457
+ return {
3458
+ ...col,
3459
+ meta: { ...col.meta, description: { type: 'text' as const, content: 'Total hits' } },
3460
+ };
3461
+ }
3462
+
3463
+ if (key === 'status') {
3464
+ return {
3465
+ ...col,
3466
+ meta: { ...col.meta, description: { type: 'tooltip' as const, content: 'Resolution state' } },
3467
+ };
3468
+ }
3469
+
3470
+ if (key === 'lastEdited') {
3471
+ return {
3472
+ ...col,
3473
+ meta: { ...col.meta, description: { type: 'tooltip' as const, content: 'Last modified' } },
3474
+ };
3475
+ }
3476
+
3477
+ if (key === 'lastSynced') {
3478
+ return {
3479
+ ...col,
3480
+ meta: { ...col.meta, description: { type: 'tooltip' as const, content: 'Last sync time' } },
3481
+ };
3482
+ }
3483
+
3484
+ return col;
3485
+ });
3486
+
3487
+ // ---------------------------------------------------------------------------
3488
+ // Infinite scroll — hook for stories
3489
+ // ---------------------------------------------------------------------------
3490
+
3491
+ const INFINITE_PAGE_SIZE = 50;
3492
+ const INFINITE_MAX_ITEMS = 500;
3493
+ const BIDIRECTIONAL_TOTAL = 500;
3494
+ const BIDIRECTIONAL_PAGE_SIZE = 50;
3495
+ const BIDIRECTIONAL_INITIAL_ANCHOR_INDEX = 250;
3496
+
3497
+ export const useInfiniteData = () => {
3498
+ const allData = useMemo(() => createLargeSecurityEvents(INFINITE_MAX_ITEMS), []);
3499
+ const [data, setData] = useState(() => allData.slice(0, INFINITE_PAGE_SIZE));
3500
+ const [isFetching, setIsFetching] = useState(false);
3501
+ const hasMore = data.length < allData.length;
3502
+
3503
+ const fetchNextPage = useCallback(() => {
3504
+ if (isFetching || !hasMore) return;
3505
+ setIsFetching(true);
3506
+
3507
+ // Simulate network delay
3508
+ setTimeout(() => {
3509
+ setData(prev => allData.slice(0, prev.length + INFINITE_PAGE_SIZE));
3510
+ setIsFetching(false);
3511
+ }, 800);
3512
+ }, [isFetching, hasMore, allData]);
3513
+
3514
+ return { data, isFetching, hasMore, totalItems: INFINITE_MAX_ITEMS, fetchNextPage };
3515
+ };
3516
+
3517
+ /**
3518
+ * Mock for bidirectional infinite scroll: opens a window around an anchor row
3519
+ * and exposes cursor-style fetchers for both directions.
3520
+ */
3521
+ export const useBidirectionalData = () => {
3522
+ const allData = useMemo(() => createLargeSecurityEvents(BIDIRECTIONAL_TOTAL), []);
3523
+
3524
+ const initialStart = Math.max(0, BIDIRECTIONAL_INITIAL_ANCHOR_INDEX - BIDIRECTIONAL_PAGE_SIZE);
3525
+ const initialEnd = Math.min(
3526
+ allData.length,
3527
+ BIDIRECTIONAL_INITIAL_ANCHOR_INDEX + BIDIRECTIONAL_PAGE_SIZE,
3528
+ );
3529
+
3530
+ const [range, setRange] = useState({ start: initialStart, end: initialEnd });
3531
+ // Separate flags so each direction drives only its own loader, mirroring a
3532
+ // real bidirectional source where scroll-up and scroll-down fetch in parallel.
3533
+ const [isFetchingPrev, setIsFetchingPrev] = useState(false);
3534
+ const [isFetchingNext, setIsFetchingNext] = useState(false);
3535
+
3536
+ const data = useMemo(() => allData.slice(range.start, range.end), [allData, range]);
3537
+ const anchorId = allData[BIDIRECTIONAL_INITIAL_ANCHOR_INDEX]?.id;
3538
+ const hasPrev = range.start > 0;
3539
+ const hasNext = range.end < allData.length;
3540
+
3541
+ const fetchPrevPage = useCallback(() => {
3542
+ if (isFetchingPrev || range.start <= 0) return;
3543
+ setIsFetchingPrev(true);
3544
+ setTimeout(() => {
3545
+ setRange(prev => ({ ...prev, start: Math.max(0, prev.start - BIDIRECTIONAL_PAGE_SIZE) }));
3546
+ setIsFetchingPrev(false);
3547
+ }, 600);
3548
+ }, [isFetchingPrev, range.start]);
3549
+
3550
+ const fetchNextPage = useCallback(() => {
3551
+ if (isFetchingNext || range.end >= allData.length) return;
3552
+ setIsFetchingNext(true);
3553
+ setTimeout(() => {
3554
+ setRange(prev => ({
3555
+ ...prev,
3556
+ end: Math.min(allData.length, prev.end + BIDIRECTIONAL_PAGE_SIZE),
3557
+ }));
3558
+ setIsFetchingNext(false);
3559
+ }, 600);
3560
+ }, [isFetchingNext, range.end, allData.length]);
3561
+
3562
+ return {
3563
+ data,
3564
+ anchorId,
3565
+ isFetchingPrev,
3566
+ isFetchingNext,
3567
+ hasPrev,
3568
+ hasNext,
3569
+ fetchPrevPage,
3570
+ fetchNextPage,
3571
+ };
3572
+ };
3573
+ ```
3574
+
3575
+ ### ../utils/abbreviateNumber.ts
3576
+
3577
+ ```tsx
3578
+ /**
3579
+ * Suffix tiers: [minAbsValue, divisor, suffix].
3580
+ * Sorted descending so the first match wins.
3581
+ *
3582
+ * Boundaries use "rounding-aware" thresholds:
3583
+ * 999,500 rounds to 1M (not 999.5k), 999,499 stays 999k.
3584
+ */
3585
+ const tiers: [threshold: number, divisor: number, suffix: string][] = [
3586
+ [999_500_000_000, 1e12, 'T'],
3587
+ [999_500_000, 1e9, 'B'],
3588
+ [999_500, 1e6, 'M'],
3589
+ [1_000, 1e3, 'k'],
3590
+ ];
3591
+
3592
+ const fullFormatter = new Intl.NumberFormat('en-US');
3593
+
3594
+ /**
3595
+ * Abbreviate a number for table cells: `12042` → `"12k"`, `59614283` → `"59.6M"`.
3596
+ *
3597
+ * Rules (from the Number display & abbreviation guide):
3598
+ * - Suffixes: k (10³), M (10⁶), B (10⁹), T (10¹²) — k lowercase, rest uppercase, no space
3599
+ * - Boundary: moves to next suffix when rounding would reach 1000×
3600
+ * - Precision: ≥100 → 0 decimals; 10–99 → 1 decimal (drop .0); 1–9.9 → 1 decimal
3601
+ * - Negative numbers supported
3602
+ */
3603
+ export const abbreviateNumber = (value: number): string => {
3604
+ if (value === 0) return '0';
3605
+
3606
+ const sign = value < 0 ? '-' : '';
3607
+ const abs = Math.abs(value);
3608
+
3609
+ if (abs < 1_000) return String(value);
3610
+
3611
+ const tier = tiers.find(([threshold]) => abs >= threshold);
3612
+
3613
+ if (!tier) return String(value);
3614
+
3615
+ const [, divisor, suffix] = tier;
3616
+ const scaled = abs / divisor;
3617
+
3618
+ // ≥100 → 0 decimals; <100 → 1 decimal (drop trailing .0)
3619
+ const precision = scaled >= 100 ? 0 : 1;
3620
+ const rounded = Math.round(scaled * 10 ** precision) / 10 ** precision;
3621
+ const formatted = rounded % 1 === 0 ? String(rounded) : rounded.toFixed(1);
3622
+
3623
+ return `${sign}${formatted}${suffix}`;
3624
+ };
3625
+
3626
+ /**
3627
+ * Format a number with thousand separators for tooltips: `59614283` → `"59,614,283"`.
3628
+ */
3629
+ export const formatFullNumber = (value: number): string => fullFormatter.format(value);
3630
+
3631
+ /**
3632
+ * Format a percentage with boundary safety:
3633
+ * - Never show 100% when real value < 100 → ">99%" (or ">99.9%" etc.)
3634
+ * - Never show 0% when real value > 0 → "<1%" (or "<0.1%" etc.)
3635
+ * - `decimals` controls fractional precision (default 0)
3636
+ */
3637
+ export const formatPercent = (value: number, decimals = 0): string => {
3638
+ if (value === 0) return '0%';
3639
+ if (value === 100) return '100%';
3640
+
3641
+ const factor = 10 ** decimals;
3642
+ const rounded = Math.round(value * factor) / factor;
3643
+
3644
+ // Boundary safety: value > 0 but rounds to 0
3645
+ if (value > 0 && rounded === 0) {
3646
+ const threshold = 1 / factor;
3647
+ return `<${threshold}%`;
3648
+ }
3649
+
3650
+ // Boundary safety: value < 100 but rounds to 100
3651
+ if (value < 100 && rounded === 100) {
3652
+ const max = 100 - 1 / factor;
3653
+ return `>${decimals > 0 ? max.toFixed(decimals) : String(max)}%`;
3654
+ }
3655
+
3656
+ return `${decimals > 0 ? rounded.toFixed(decimals) : String(rounded)}%`;
3657
+ };
3658
+
3659
+ /**
3660
+ * Byte-formatting tiers: [minValue, divisor, suffix].
3661
+ * Uses decimal (SI) units: KB = 1000, MB = 1e6, etc.
3662
+ */
3663
+ const byteTiers: [threshold: number, divisor: number, suffix: string][] = [
3664
+ [999_500_000_000, 1e12, 'TB'],
3665
+ [999_500_000, 1e9, 'GB'],
3666
+ [999_500, 1e6, 'MB'],
3667
+ [1_000, 1e3, 'KB'],
3668
+ ];
3669
+
3670
+ /**
3671
+ * Format bytes using decimal (SI) units: 512 B, 3.4 KB, 12.7 MB, 2.3 GB, 1.1 TB.
3672
+ * Same decimal precision rules as abbreviateNumber.
3673
+ * Returns `{ display, full }` where `full` = "2,345,678,901 bytes".
3674
+ */
3675
+ export const formatBytes = (value: number): { display: string; full: string } => {
3676
+ const full = `${fullFormatter.format(value)} bytes`;
3677
+
3678
+ if (value < 1_000) {
3679
+ return { display: `${value} B`, full };
3680
+ }
3681
+
3682
+ const tier = byteTiers.find(([threshold]) => value >= threshold);
3683
+
3684
+ if (!tier) {
3685
+ return { display: `${value} B`, full };
3686
+ }
3687
+
3688
+ const [, divisor, suffix] = tier;
3689
+ const scaled = value / divisor;
3690
+
3691
+ const precision = scaled >= 100 ? 0 : 1;
3692
+ const rounded = Math.round(scaled * 10 ** precision) / 10 ** precision;
3693
+ const formatted = rounded % 1 === 0 ? String(rounded) : rounded.toFixed(1);
3694
+
3695
+ return { display: `${formatted} ${suffix}`, full };
3696
+ };
3697
+ ```