railwatch 0.6.1 → 0.7.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 (345) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/app/frontend/components/app-content.tsx +26 -0
  4. data/app/frontend/components/app-logo-icon.tsx +23 -0
  5. data/app/frontend/components/app-logo.tsx +14 -0
  6. data/app/frontend/components/app-shell.tsx +38 -0
  7. data/app/frontend/components/app-sidebar-header.tsx +54 -0
  8. data/app/frontend/components/app-sidebar.tsx +94 -0
  9. data/app/frontend/components/app-wordmark.tsx +29 -0
  10. data/app/frontend/components/breadcrumbs.tsx +46 -0
  11. data/app/frontend/components/icon.tsx +16 -0
  12. data/app/frontend/components/input-error.tsx +18 -0
  13. data/app/frontend/components/nav-footer.tsx +46 -0
  14. data/app/frontend/components/nav-main.tsx +41 -0
  15. data/app/frontend/components/nav-user.tsx +50 -0
  16. data/app/frontend/components/railwatch/chart-hover.tsx +34 -0
  17. data/app/frontend/components/railwatch/chart-panel.tsx +170 -0
  18. data/app/frontend/components/railwatch/code.tsx +28 -0
  19. data/app/frontend/components/railwatch/command-palette.tsx +132 -0
  20. data/app/frontend/components/railwatch/copy-block.tsx +52 -0
  21. data/app/frontend/components/railwatch/copy-id.tsx +49 -0
  22. data/app/frontend/components/railwatch/cursor-load-more.tsx +39 -0
  23. data/app/frontend/components/railwatch/data-table.tsx +195 -0
  24. data/app/frontend/components/railwatch/digit-roll.tsx +312 -0
  25. data/app/frontend/components/railwatch/empty-state.tsx +159 -0
  26. data/app/frontend/components/railwatch/env-nav.tsx +130 -0
  27. data/app/frontend/components/railwatch/env-switcher.tsx +145 -0
  28. data/app/frontend/components/railwatch/filter-bar.tsx +153 -0
  29. data/app/frontend/components/railwatch/flamegraph.tsx +324 -0
  30. data/app/frontend/components/railwatch/frames.tsx +295 -0
  31. data/app/frontend/components/railwatch/json-viewer.tsx +159 -0
  32. data/app/frontend/components/railwatch/live-dot.tsx +78 -0
  33. data/app/frontend/components/railwatch/live-toggle.tsx +76 -0
  34. data/app/frontend/components/railwatch/metric-chart.tsx +112 -0
  35. data/app/frontend/components/railwatch/mobile-page-switcher.tsx +89 -0
  36. data/app/frontend/components/railwatch/onboarding.tsx +91 -0
  37. data/app/frontend/components/railwatch/origin-identity.tsx +84 -0
  38. data/app/frontend/components/railwatch/page-header.tsx +42 -0
  39. data/app/frontend/components/railwatch/percentile-picker.tsx +18 -0
  40. data/app/frontend/components/railwatch/relative-time.tsx +18 -0
  41. data/app/frontend/components/railwatch/saved-views.tsx +232 -0
  42. data/app/frontend/components/railwatch/segmented.tsx +64 -0
  43. data/app/frontend/components/railwatch/series-chart.tsx +400 -0
  44. data/app/frontend/components/railwatch/shortcuts-dialog.tsx +89 -0
  45. data/app/frontend/components/railwatch/signal-toast.tsx +121 -0
  46. data/app/frontend/components/railwatch/sort-header.tsx +37 -0
  47. data/app/frontend/components/railwatch/source-link.tsx +57 -0
  48. data/app/frontend/components/railwatch/sparkline-cell.tsx +14 -0
  49. data/app/frontend/components/railwatch/sql-block.tsx +116 -0
  50. data/app/frontend/components/railwatch/stat.tsx +173 -0
  51. data/app/frontend/components/railwatch/status-badge.tsx +79 -0
  52. data/app/frontend/components/railwatch/step-picker.tsx +57 -0
  53. data/app/frontend/components/railwatch/timeline.tsx +405 -0
  54. data/app/frontend/components/railwatch/utilisation-bar.tsx +44 -0
  55. data/app/frontend/components/railwatch/window-picker.tsx +147 -0
  56. data/app/frontend/components/ui/avatar.tsx +51 -0
  57. data/app/frontend/components/ui/badge.tsx +46 -0
  58. data/app/frontend/components/ui/breadcrumb.tsx +109 -0
  59. data/app/frontend/components/ui/button.tsx +60 -0
  60. data/app/frontend/components/ui/card.tsx +92 -0
  61. data/app/frontend/components/ui/chart.tsx +374 -0
  62. data/app/frontend/components/ui/checkbox.tsx +32 -0
  63. data/app/frontend/components/ui/collapsible.tsx +31 -0
  64. data/app/frontend/components/ui/command.tsx +182 -0
  65. data/app/frontend/components/ui/dialog.tsx +158 -0
  66. data/app/frontend/components/ui/dropdown-menu.tsx +255 -0
  67. data/app/frontend/components/ui/hover-card.tsx +42 -0
  68. data/app/frontend/components/ui/input.tsx +21 -0
  69. data/app/frontend/components/ui/label.tsx +24 -0
  70. data/app/frontend/components/ui/popover.tsx +87 -0
  71. data/app/frontend/components/ui/select.tsx +185 -0
  72. data/app/frontend/components/ui/separator.tsx +28 -0
  73. data/app/frontend/components/ui/sheet.tsx +137 -0
  74. data/app/frontend/components/ui/sidebar.tsx +726 -0
  75. data/app/frontend/components/ui/skeleton.tsx +13 -0
  76. data/app/frontend/components/ui/sonner.tsx +41 -0
  77. data/app/frontend/components/ui/table.tsx +114 -0
  78. data/app/frontend/components/ui/tabs.tsx +91 -0
  79. data/app/frontend/components/ui/textarea.tsx +18 -0
  80. data/app/frontend/components/ui/tooltip.tsx +61 -0
  81. data/app/frontend/components/user-info.tsx +32 -0
  82. data/app/frontend/components/user-menu-content.tsx +94 -0
  83. data/app/frontend/entrypoints/application.css +244 -0
  84. data/app/frontend/entrypoints/inertia.tsx +95 -0
  85. data/app/frontend/fonts/CommitMono-Bold.woff2 +0 -0
  86. data/app/frontend/fonts/CommitMono-Regular.woff2 +0 -0
  87. data/app/frontend/fonts/Roboto-Black.woff2 +0 -0
  88. data/app/frontend/fonts/Roboto-Bold.woff2 +0 -0
  89. data/app/frontend/fonts/Roboto-Medium.woff2 +0 -0
  90. data/app/frontend/fonts/Roboto-Regular.woff2 +0 -0
  91. data/app/frontend/hooks/use-appearance.tsx +67 -0
  92. data/app/frontend/hooks/use-clipboard.ts +32 -0
  93. data/app/frontend/hooks/use-flash.tsx +23 -0
  94. data/app/frontend/hooks/use-hotkeys.ts +63 -0
  95. data/app/frontend/hooks/use-initials.tsx +15 -0
  96. data/app/frontend/hooks/use-live.ts +179 -0
  97. data/app/frontend/hooks/use-mobile-navigation.ts +8 -0
  98. data/app/frontend/hooks/use-mobile.ts +21 -0
  99. data/app/frontend/hooks/use-percentile.ts +24 -0
  100. data/app/frontend/hooks/use-reduced-motion.ts +25 -0
  101. data/app/frontend/hooks/use-row-nav.ts +106 -0
  102. data/app/frontend/hooks/use-step.ts +27 -0
  103. data/app/frontend/hooks/use-ticker.ts +12 -0
  104. data/app/frontend/hooks/use-window.ts +58 -0
  105. data/app/frontend/layouts/app/app-sidebar-layout.tsx +107 -0
  106. data/app/frontend/layouts/app-layout.tsx +21 -0
  107. data/app/frontend/layouts/env-layout.tsx +53 -0
  108. data/app/frontend/layouts/persistent-layout.tsx +18 -0
  109. data/app/frontend/lib/cable.ts +10 -0
  110. data/app/frontend/lib/env-pages.ts +236 -0
  111. data/app/frontend/lib/execution-path.ts +55 -0
  112. data/app/frontend/lib/explain.ts +50 -0
  113. data/app/frontend/lib/filter.ts +25 -0
  114. data/app/frontend/lib/flamegraph.ts +155 -0
  115. data/app/frontend/lib/format.ts +71 -0
  116. data/app/frontend/lib/keyboard.ts +18 -0
  117. data/app/frontend/lib/source-link.ts +217 -0
  118. data/app/frontend/lib/utils.ts +6 -0
  119. data/app/frontend/pages/alerts/index.tsx +171 -0
  120. data/app/frontend/pages/broadcasts/index.tsx +171 -0
  121. data/app/frontend/pages/cache_events/index.tsx +127 -0
  122. data/app/frontend/pages/commands/index.tsx +143 -0
  123. data/app/frontend/pages/deploys/index.tsx +94 -0
  124. data/app/frontend/pages/deploys/show.tsx +309 -0
  125. data/app/frontend/pages/deprecations/index.tsx +176 -0
  126. data/app/frontend/pages/exceptions/index.tsx +271 -0
  127. data/app/frontend/pages/executions/show.tsx +821 -0
  128. data/app/frontend/pages/issues/index.tsx +399 -0
  129. data/app/frontend/pages/issues/show.tsx +1367 -0
  130. data/app/frontend/pages/jobs/index.tsx +381 -0
  131. data/app/frontend/pages/jobs/klass.tsx +215 -0
  132. data/app/frontend/pages/llm_calls/index.tsx +664 -0
  133. data/app/frontend/pages/logs/index.tsx +303 -0
  134. data/app/frontend/pages/mails/index.tsx +218 -0
  135. data/app/frontend/pages/notifications/index.tsx +161 -0
  136. data/app/frontend/pages/outgoing_requests/index.tsx +309 -0
  137. data/app/frontend/pages/overview/show.tsx +333 -0
  138. data/app/frontend/pages/people/index.tsx +102 -0
  139. data/app/frontend/pages/people/show.tsx +149 -0
  140. data/app/frontend/pages/processes/index.tsx +335 -0
  141. data/app/frontend/pages/profiles/index.tsx +110 -0
  142. data/app/frontend/pages/profiles/show.tsx +105 -0
  143. data/app/frontend/pages/queries/index.tsx +492 -0
  144. data/app/frontend/pages/queries/show.tsx +276 -0
  145. data/app/frontend/pages/releases/index.tsx +190 -0
  146. data/app/frontend/pages/releases/release-health.tsx +68 -0
  147. data/app/frontend/pages/releases/release-path.ts +25 -0
  148. data/app/frontend/pages/releases/show.tsx +262 -0
  149. data/app/frontend/pages/requests/index.tsx +235 -0
  150. data/app/frontend/pages/requests/route.tsx +159 -0
  151. data/app/frontend/pages/scheduled_tasks/index.tsx +220 -0
  152. data/app/frontend/pages/spans/index.tsx +229 -0
  153. data/app/frontend/pages/spans/show.tsx +220 -0
  154. data/app/frontend/pages/storage_ops/index.tsx +150 -0
  155. data/app/frontend/pages/tenants/index.tsx +236 -0
  156. data/app/frontend/pages/tenants/show.tsx +477 -0
  157. data/app/frontend/pages/tenants/tenant-path.ts +26 -0
  158. data/app/frontend/pages/thresholds/index.tsx +414 -0
  159. data/app/frontend/pages/traces/show.tsx +208 -0
  160. data/app/frontend/pages/transactions/index.tsx +220 -0
  161. data/app/frontend/pages/view_renders/index.tsx +241 -0
  162. data/app/frontend/pages/visits/index.tsx +321 -0
  163. data/app/frontend/public/icon.png +0 -0
  164. data/app/frontend/public/icon.svg +5 -0
  165. data/app/frontend/routes/index.d.ts +2136 -0
  166. data/app/frontend/routes/index.js +2112 -0
  167. data/app/frontend/types/globals.d.ts +9 -0
  168. data/app/frontend/types/index.ts +288 -0
  169. data/app/frontend/types/vite-env.d.ts +9 -0
  170. data/lib/railwatch/version.rb +1 -1
  171. data/lib/railwatch.rb +8 -3
  172. data/public/railwatch/assets/app-layout-LvxN9xSS.js +41 -0
  173. data/public/railwatch/assets/application-Cc5v_Vk9.css +1 -0
  174. data/public/railwatch/assets/{arrow-up-CLQ-7heQ.js → arrow-up-DG01k9AL.js} +1 -1
  175. data/public/railwatch/assets/{braces-rSYydpLY.js → braces-C3w7pfxS.js} +1 -1
  176. data/public/railwatch/assets/{card-DPjFKfen.js → card-Dpwlr0uY.js} +1 -1
  177. data/public/railwatch/assets/{chart-DWh7l8yM.js → chart-DJHv3R7W.js} +1 -1
  178. data/public/railwatch/assets/{chart-hover-CfoZUY4J.js → chart-hover-CEH-DtXO.js} +1 -1
  179. data/public/railwatch/assets/{chart-panel-CC49WTQL.js → chart-panel-CUdIzJvk.js} +1 -1
  180. data/public/railwatch/assets/code-BRZprwun.js +1 -0
  181. data/public/railwatch/assets/copy-id-CwhGGiwF.js +1 -0
  182. data/public/railwatch/assets/cursor-load-more-uF1_955Q.js +1 -0
  183. data/public/railwatch/assets/data-table-DYi-N0hm.js +1 -0
  184. data/public/railwatch/assets/env-layout-HdcOaPPX.js +1 -0
  185. data/public/railwatch/assets/{execution-path-FAuIzOXB.js → execution-path-CElXXFO_.js} +1 -1
  186. data/public/railwatch/assets/filter-bar-DEv0G_34.js +1 -0
  187. data/public/railwatch/assets/{flamegraph-EaGkP2NT.js → flamegraph-SKD9yxW9.js} +2 -2
  188. data/public/railwatch/assets/frames-CUpyJQDy.js +1 -0
  189. data/public/railwatch/assets/index-2W-ppwap.js +1 -0
  190. data/public/railwatch/assets/index-74ocuU5d.js +1 -0
  191. data/public/railwatch/assets/{index-C8Cggnbw.js → index-B8NA62Lz.js} +2 -2
  192. data/public/railwatch/assets/index-BBCEkLqp.js +1 -0
  193. data/public/railwatch/assets/index-BDcglJzX.js +1 -0
  194. data/public/railwatch/assets/index-B_KFb9LS.js +1 -0
  195. data/public/railwatch/assets/index-BqK_VKom.js +1 -0
  196. data/public/railwatch/assets/index-BxCXBHP2.js +1 -0
  197. data/public/railwatch/assets/index-C1jc1OoH.js +1 -0
  198. data/public/railwatch/assets/index-C7zREldp.js +1 -0
  199. data/public/railwatch/assets/index-CEetm4oC.js +1 -0
  200. data/public/railwatch/assets/index-CIaZkokF.js +1 -0
  201. data/public/railwatch/assets/index-CNH85TZp.js +1 -0
  202. data/public/railwatch/assets/index-Ce4SB5AB.js +1 -0
  203. data/public/railwatch/assets/index-CiRMSYMn.js +1 -0
  204. data/public/railwatch/assets/index-CjxXgOs-.js +1 -0
  205. data/public/railwatch/assets/index-ClL-PYEV.js +1 -0
  206. data/public/railwatch/assets/index-CteZ6fop.js +1 -0
  207. data/public/railwatch/assets/index-CvqfEExO.js +1 -0
  208. data/public/railwatch/assets/index-CxTLT-Qh.js +1 -0
  209. data/public/railwatch/assets/index-DJd3OUSd.js +1 -0
  210. data/public/railwatch/assets/index-DPD-rLp6.js +1 -0
  211. data/public/railwatch/assets/index-DYK_dps4.js +1 -0
  212. data/public/railwatch/assets/index-DrayARrI.js +1 -0
  213. data/public/railwatch/assets/index-HRXGir-0.js +1 -0
  214. data/public/railwatch/assets/index-UJOUALr_.js +1 -0
  215. data/public/railwatch/assets/index-d-3J7gQ5.js +1 -0
  216. data/public/railwatch/assets/index-oQrvBpOm.js +1 -0
  217. data/public/railwatch/assets/inertia-FoqYZAk8.js +97 -0
  218. data/public/railwatch/assets/json-viewer-CxqMojEm.js +1 -0
  219. data/public/railwatch/assets/klass-DEF-Vat5.js +1 -0
  220. data/public/railwatch/assets/origin-identity-Y6gvfQVR.js +1 -0
  221. data/public/railwatch/assets/{percentile-picker-CeQgllxD.js → percentile-picker-E2aUdLxx.js} +1 -1
  222. data/public/railwatch/assets/relative-time-DcJbkKlN.js +1 -0
  223. data/public/railwatch/assets/{release-health-uP-GF3_V.js → release-health-C8WSCDKD.js} +1 -1
  224. data/public/railwatch/assets/route-qO5vX1p-.js +1 -0
  225. data/public/railwatch/assets/{segmented-BeIe4uqk.js → segmented-BWee8uc9.js} +1 -1
  226. data/public/railwatch/assets/select-G7rNBzv0.js +1 -0
  227. data/public/railwatch/assets/series-chart-C5nXIUc5.js +1 -0
  228. data/public/railwatch/assets/show-BEWisLQw.js +1 -0
  229. data/public/railwatch/assets/show-BEa9PGu1.js +2 -0
  230. data/public/railwatch/assets/{show-YsNpqbcI.js → show-BiT_Aloc.js} +3 -3
  231. data/public/railwatch/assets/show-BpCO49d3.js +1 -0
  232. data/public/railwatch/assets/show-CH7yxt7R.js +1 -0
  233. data/public/railwatch/assets/show-CTO8Lvd2.js +1 -0
  234. data/public/railwatch/assets/show-DnYsDMUt.js +2 -0
  235. data/public/railwatch/assets/show-DyzHM527.js +1 -0
  236. data/public/railwatch/assets/show-Nfpxs8su.js +1 -0
  237. data/public/railwatch/assets/show-ddjUMo9G.js +1 -0
  238. data/public/railwatch/assets/show-ksjbSzDH.js +1 -0
  239. data/public/railwatch/assets/{sort-header-mEJUM3Av.js → sort-header-uOq5VlrB.js} +1 -1
  240. data/public/railwatch/assets/{sparkline-cell-Dwg7awwh.js → sparkline-cell-DgGij0cu.js} +1 -1
  241. data/public/railwatch/assets/stat-BTzNKTm2.js +1 -0
  242. data/public/railwatch/assets/status-badge-BDkOXa-C.js +1 -0
  243. data/public/railwatch/assets/{tenant-path-CQoP-BeF.js → tenant-path-DU0ezKSU.js} +1 -1
  244. data/public/railwatch/assets/{use-clipboard-DNefo-ky.js → use-clipboard-DoENmlWY.js} +1 -1
  245. data/public/railwatch/assets/{use-live-DMTuhKfB.js → use-live-CE_fN5_6.js} +1 -1
  246. data/public/railwatch/manifest.json +612 -1591
  247. metadata +242 -117
  248. data/public/railwatch/assets/app-layout-Zc0v-hYh.js +0 -1
  249. data/public/railwatch/assets/app-wordmark-BA_60AVb.js +0 -1
  250. data/public/railwatch/assets/appearance-CcfP9tZ7.js +0 -1
  251. data/public/railwatch/assets/application-BrN3Sz94.css +0 -1
  252. data/public/railwatch/assets/auth-layout-C05gEBIQ.js +0 -1
  253. data/public/railwatch/assets/badge-DRae8XwK.js +0 -1
  254. data/public/railwatch/assets/checkbox-DPkLUiwM.js +0 -1
  255. data/public/railwatch/assets/code-CLmYS6FU.js +0 -1
  256. data/public/railwatch/assets/copy-DaQMJWoT.js +0 -1
  257. data/public/railwatch/assets/copy-block-CGcXxp8J.js +0 -1
  258. data/public/railwatch/assets/copy-id-vBYHQwxg.js +0 -1
  259. data/public/railwatch/assets/cursor-load-more-Ddwxe5dZ.js +0 -1
  260. data/public/railwatch/assets/data-table-CzKTEE-O.js +0 -1
  261. data/public/railwatch/assets/edit-B1kmWkzd.js +0 -8
  262. data/public/railwatch/assets/edit-D9cx4pbG.js +0 -1
  263. data/public/railwatch/assets/edit-yv6j9p-V.js +0 -1
  264. data/public/railwatch/assets/empty-state-CW4wclK_.js +0 -1
  265. data/public/railwatch/assets/env-layout-Kz7wks1x.js +0 -1
  266. data/public/railwatch/assets/filter-bar-CJDjWFib.js +0 -1
  267. data/public/railwatch/assets/frames-zZuIaNH9.js +0 -1
  268. data/public/railwatch/assets/google-sign-in-button-2_zgbgVy.js +0 -1
  269. data/public/railwatch/assets/index-B49SWz7K.js +0 -1
  270. data/public/railwatch/assets/index-BHlY4wKe.js +0 -1
  271. data/public/railwatch/assets/index-BZpPtyFY.js +0 -1
  272. data/public/railwatch/assets/index-BeVK6jCL.js +0 -1
  273. data/public/railwatch/assets/index-BfbSo01U.js +0 -1
  274. data/public/railwatch/assets/index-BfgncAv6.js +0 -1
  275. data/public/railwatch/assets/index-BhNszK1k.js +0 -1
  276. data/public/railwatch/assets/index-Bu01uWvw.js +0 -1
  277. data/public/railwatch/assets/index-C1s_hK3p.js +0 -1
  278. data/public/railwatch/assets/index-CBip6V4z.js +0 -1
  279. data/public/railwatch/assets/index-CMJGss5R.js +0 -1
  280. data/public/railwatch/assets/index-CRo3yK20.js +0 -1
  281. data/public/railwatch/assets/index-CWB_p2J8.js +0 -1
  282. data/public/railwatch/assets/index-CWHpndGc.js +0 -1
  283. data/public/railwatch/assets/index-Ca_S4Sc3.js +0 -1
  284. data/public/railwatch/assets/index-CanPDDOa.js +0 -1
  285. data/public/railwatch/assets/index-Cie90Yat.js +0 -1
  286. data/public/railwatch/assets/index-CiepQ_pR.js +0 -1
  287. data/public/railwatch/assets/index-Cm1uCIGN.js +0 -1
  288. data/public/railwatch/assets/index-CzitnnSC.js +0 -1
  289. data/public/railwatch/assets/index-DEUfClv3.js +0 -1
  290. data/public/railwatch/assets/index-DJKwo-mI.js +0 -1
  291. data/public/railwatch/assets/index-DK6y0YHp.js +0 -1
  292. data/public/railwatch/assets/index-DMNPpLH9.js +0 -1
  293. data/public/railwatch/assets/index-DZO1mSfX.js +0 -1
  294. data/public/railwatch/assets/index-Db5wj3M5.js +0 -1
  295. data/public/railwatch/assets/index-Dcy5WktB.js +0 -1
  296. data/public/railwatch/assets/index-DvG0-7Lx.js +0 -1
  297. data/public/railwatch/assets/index-Dvj1wuka.js +0 -1
  298. data/public/railwatch/assets/index-KyZX46qX.js +0 -1
  299. data/public/railwatch/assets/index-Ze-KP-sl.js +0 -1
  300. data/public/railwatch/assets/index-mhRbWLBM.js +0 -1
  301. data/public/railwatch/assets/index-x099JL5f.js +0 -1
  302. data/public/railwatch/assets/index-x28zb_nf.js +0 -1
  303. data/public/railwatch/assets/inertia-Cuyz2ZHO.js +0 -97
  304. data/public/railwatch/assets/input-error-hog6gGxg.js +0 -1
  305. data/public/railwatch/assets/json-viewer-DH2W9HXf.js +0 -1
  306. data/public/railwatch/assets/klass-DHbDelLk.js +0 -1
  307. data/public/railwatch/assets/label-DdCBgiUn.js +0 -1
  308. data/public/railwatch/assets/layout-Cueyl7c5.js +0 -1
  309. data/public/railwatch/assets/live-dot-BZgYTYdt.js +0 -41
  310. data/public/railwatch/assets/nav-BSSGObDZ.js +0 -1
  311. data/public/railwatch/assets/new-B8FSb8Bl.js +0 -1
  312. data/public/railwatch/assets/new-C39_v2Ll.js +0 -1
  313. data/public/railwatch/assets/new-DVOPwaC5.js +0 -1
  314. data/public/railwatch/assets/new-Dd40nvJR.js +0 -1
  315. data/public/railwatch/assets/new-SxnYe1SE.js +0 -1
  316. data/public/railwatch/assets/new-eP5vKD3Y.js +0 -1
  317. data/public/railwatch/assets/onboarding-CUpZl5KB.js +0 -1
  318. data/public/railwatch/assets/origin-identity-BYt2uiuo.js +0 -1
  319. data/public/railwatch/assets/relative-time-D5UbF4oO.js +0 -1
  320. data/public/railwatch/assets/route-DQAY8JEr.js +0 -1
  321. data/public/railwatch/assets/select-2R16457Z.js +0 -1
  322. data/public/railwatch/assets/separator-D5I0UCB5.js +0 -1
  323. data/public/railwatch/assets/series-chart-gnrzhmM6.js +0 -1
  324. data/public/railwatch/assets/show-2BkeNRUC.js +0 -2
  325. data/public/railwatch/assets/show-B0X1hRQH.js +0 -1
  326. data/public/railwatch/assets/show-BKUV5l5q.js +0 -1
  327. data/public/railwatch/assets/show-BQJD_CsF.js +0 -6
  328. data/public/railwatch/assets/show-BhmD6Sxp.js +0 -1
  329. data/public/railwatch/assets/show-C3KcnVo2.js +0 -1
  330. data/public/railwatch/assets/show-C95cHd06.js +0 -2
  331. data/public/railwatch/assets/show-CdB4uVQS.js +0 -1
  332. data/public/railwatch/assets/show-CoCqcVIp.js +0 -1
  333. data/public/railwatch/assets/show-D2LqjEsU.js +0 -1
  334. data/public/railwatch/assets/show-DNpnKyTh.js +0 -1
  335. data/public/railwatch/assets/show-DOlTxbig.js +0 -1
  336. data/public/railwatch/assets/show-DQCkttL8.js +0 -1
  337. data/public/railwatch/assets/show-DcxesipC.js +0 -1
  338. data/public/railwatch/assets/show-IGJoc_X0.js +0 -1
  339. data/public/railwatch/assets/show-qNV6H8SH.js +0 -2
  340. data/public/railwatch/assets/stat-ZtxGU8lE.js +0 -1
  341. data/public/railwatch/assets/status-badge-CH5P-Xkj.js +0 -1
  342. data/public/railwatch/assets/text-link-DHJ8BXx5.js +0 -1
  343. data/public/railwatch/assets/textarea-BeQtQyl5.js +0 -1
  344. data/public/railwatch/assets/timeline-CaKQVu48.js +0 -1
  345. data/public/railwatch/assets/transition-ksDpqhKJ.js +0 -5
@@ -0,0 +1,64 @@
1
+ import type { ReactNode } from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ // Nightwatch's period bar: a bordered pill holding mono buttons, the selected
6
+ // one filled blue. Used for time windows, percentiles, and status tabs so
7
+ // every segmented choice in the app looks the same.
8
+ export function Segmented({
9
+ children,
10
+ className,
11
+ }: {
12
+ children: ReactNode
13
+ className?: string
14
+ }) {
15
+ return (
16
+ <div
17
+ className={cn(
18
+ "bg-card flex h-8 shrink-0 items-center gap-px rounded-lg border p-0.5",
19
+ className,
20
+ )}
21
+ >
22
+ {children}
23
+ </div>
24
+ )
25
+ }
26
+
27
+ export function SegmentedItem({
28
+ active,
29
+ onClick,
30
+ children,
31
+ className,
32
+ mono = true,
33
+ disabled,
34
+ "aria-label": ariaLabel,
35
+ }: {
36
+ active: boolean
37
+ onClick: () => void
38
+ children: ReactNode
39
+ className?: string
40
+ mono?: boolean
41
+ disabled?: boolean
42
+ "aria-label"?: string
43
+ }) {
44
+ return (
45
+ <button
46
+ type="button"
47
+ onClick={onClick}
48
+ disabled={disabled}
49
+ aria-pressed={active}
50
+ aria-label={ariaLabel}
51
+ className={cn(
52
+ "flex h-full min-w-8 items-center justify-center gap-1.5 rounded-md border px-2.5 text-xs whitespace-nowrap transition-colors",
53
+ mono && "font-mono uppercase",
54
+ active
55
+ ? "bg-primary border-primary text-primary-foreground"
56
+ : "text-muted-foreground hover:bg-accent hover:text-foreground border-transparent",
57
+ disabled && "cursor-not-allowed opacity-50",
58
+ className,
59
+ )}
60
+ >
61
+ {children}
62
+ </button>
63
+ )
64
+ }
@@ -0,0 +1,400 @@
1
+ import {
2
+ Area,
3
+ AreaChart,
4
+ Bar,
5
+ BarChart,
6
+ CartesianGrid,
7
+ Line,
8
+ LineChart,
9
+ ReferenceLine,
10
+ XAxis,
11
+ YAxis,
12
+ } from "recharts"
13
+
14
+ import { useChartHover } from "@/components/railwatch/chart-hover"
15
+ import {
16
+ type ChartConfig,
17
+ ChartContainer,
18
+ ChartTooltip,
19
+ ChartTooltipContent,
20
+ } from "@/components/ui/chart"
21
+ import { ms } from "@/lib/format"
22
+ import type {
23
+ DeployMarker,
24
+ Percentile,
25
+ SeriesPoint,
26
+ SessionStatusPoint,
27
+ } from "@/types"
28
+
29
+ // Status vocabulary, same as the badges: ok is quiet grey so the amber and
30
+ // red slices are what the eye lands on; latency lines use the accent for
31
+ // the chosen percentile and a muted amber for the average.
32
+ const config = {
33
+ count: { label: "OK", color: "var(--ok)" },
34
+ errors: { label: "5xx", color: "var(--danger)" },
35
+ client_errors: { label: "4xx", color: "var(--warning)" },
36
+ p50: { label: "p50", color: "var(--primary)" },
37
+ p95: { label: "p95", color: "var(--primary)" },
38
+ p99: { label: "p99", color: "var(--primary)" },
39
+ avg: { label: "avg", color: "var(--warning)" },
40
+ } satisfies ChartConfig
41
+
42
+ // Bucket width in ms, read off the data so the axis needs no page context:
43
+ // a minute chart labels ticks by time, a day chart by date.
44
+ function bucketWidth(data: { t: string }[]) {
45
+ if (data.length < 2) return 0
46
+ return new Date(data[1].t).getTime() - new Date(data[0].t).getTime()
47
+ }
48
+
49
+ const DAY = 24 * 60 * 60 * 1000
50
+
51
+ function tickFormatter(data: { t: string }[]) {
52
+ const width = bucketWidth(data)
53
+ const span =
54
+ data.length > 1
55
+ ? new Date(data[data.length - 1].t).getTime() -
56
+ new Date(data[0].t).getTime()
57
+ : 0
58
+ if (width >= DAY || span > 2 * DAY)
59
+ return (t: string) =>
60
+ new Date(t).toLocaleDateString(undefined, {
61
+ month: "short",
62
+ day: "numeric",
63
+ })
64
+ return (t: string) =>
65
+ new Date(t).toLocaleTimeString(undefined, {
66
+ hour: "2-digit",
67
+ minute: "2-digit",
68
+ })
69
+ }
70
+
71
+ function stamp(t: string) {
72
+ const d = new Date(t)
73
+ return d.toLocaleString(undefined, {
74
+ day: "2-digit",
75
+ month: "short",
76
+ hour: "2-digit",
77
+ minute: "2-digit",
78
+ })
79
+ }
80
+
81
+ const axisTick = { fontSize: 10, fontFamily: "var(--font-mono)" }
82
+
83
+ // A line only joins neighbouring points, so a bucket with data between two
84
+ // quiet ones (common at a minute's width) would draw nothing. Mark those
85
+ // lone points with a dot; everything else stays a plain line. Recharts can
86
+ // call this with an index past the array it was built over (a live reload
87
+ // swapping in a shorter series mid-render did, LC-43), so neighbours are
88
+ // looked up defensively rather than assumed.
89
+ export function loneDot(
90
+ data: Record<string, unknown>[],
91
+ key: string,
92
+ color: string,
93
+ ) {
94
+ const LoneDot = (props: { cx?: number; cy?: number; index?: number }) => {
95
+ const { cx, cy, index } = props
96
+ if (cx == null || cy == null || index == null) return <g key={index} />
97
+ const before = data[index - 1]?.[key] ?? null
98
+ const after = data[index + 1]?.[key] ?? null
99
+ if (before != null || after != null) return <g key={index} />
100
+ return <circle key={index} cx={cx} cy={cy} r={2} fill={color} />
101
+ }
102
+ return LoneDot
103
+ }
104
+
105
+ function markers(deploys: DeployMarker[] = []) {
106
+ return deploys.map((d) => (
107
+ <ReferenceLine
108
+ key={d.deploy}
109
+ x={new Date(d.at).toISOString()}
110
+ stroke="var(--primary)"
111
+ strokeDasharray="3 3"
112
+ label={{
113
+ value: d.ref,
114
+ position: "insideTopRight",
115
+ fontSize: 9,
116
+ fill: "var(--primary)",
117
+ fontFamily: "var(--font-mono)",
118
+ }}
119
+ />
120
+ ))
121
+ }
122
+
123
+ // Recharts calls onMouseMove with the hovered point's x value as
124
+ // `activeLabel` (our bucket ISO timestamp) — feed that into ChartHoverProvider
125
+ // so a table elsewhere on the page can highlight the matching row.
126
+ function useBucketHover() {
127
+ const { setHoveredKey } = useChartHover()
128
+ return {
129
+ onMouseMove: (state: { activeLabel?: string | number }) => {
130
+ if (typeof state?.activeLabel === "string")
131
+ setHoveredKey(state.activeLabel)
132
+ },
133
+ onMouseLeave: () => setHoveredKey(null),
134
+ }
135
+ }
136
+
137
+ // Bucket-range caption under a chart, like Nightwatch's "02 Nov 18:00 UTC".
138
+ function Range({ data }: { data: { t: string }[] }) {
139
+ if (data.length === 0) return null
140
+ return (
141
+ <div className="text-muted-foreground mt-1 flex justify-between font-mono text-[10px]">
142
+ <span>{stamp(data[0].t)}</span>
143
+ <span>{stamp(data[data.length - 1].t)}</span>
144
+ </div>
145
+ )
146
+ }
147
+
148
+ export function ThroughputChart({
149
+ data,
150
+ deploys,
151
+ label = "Requests",
152
+ className = "h-40",
153
+ }: {
154
+ data: SeriesPoint[]
155
+ deploys?: DeployMarker[]
156
+ label?: string
157
+ className?: string
158
+ }) {
159
+ const cfg = { ...config, count: { ...config.count, label } }
160
+ const hover = useBucketHover()
161
+ const tick = tickFormatter(data)
162
+ return (
163
+ <div>
164
+ <ChartContainer config={cfg} className={`${className} w-full`}>
165
+ <BarChart
166
+ data={data.map((d) => ({
167
+ ...d,
168
+ t: new Date(d.t).toISOString(),
169
+ ok: Math.max(0, d.count - d.errors - d.client_errors),
170
+ }))}
171
+ barCategoryGap={2}
172
+ onMouseMove={hover.onMouseMove}
173
+ onMouseLeave={hover.onMouseLeave}
174
+ >
175
+ <CartesianGrid vertical={false} strokeDasharray="2 4" />
176
+ <XAxis
177
+ dataKey="t"
178
+ tickFormatter={tick}
179
+ tickLine={false}
180
+ axisLine={false}
181
+ minTickGap={40}
182
+ tick={axisTick}
183
+ />
184
+ <YAxis
185
+ width={36}
186
+ tickLine={false}
187
+ axisLine={false}
188
+ tick={axisTick}
189
+ tickFormatter={(v) => compact(Number(v))}
190
+ />
191
+ <ChartTooltip
192
+ content={
193
+ <ChartTooltipContent labelFormatter={(v) => stamp(String(v))} />
194
+ }
195
+ />
196
+ <Bar
197
+ dataKey="ok"
198
+ name="count"
199
+ fill="var(--color-count)"
200
+ stackId="a"
201
+ />
202
+ <Bar
203
+ dataKey="client_errors"
204
+ fill="var(--color-client_errors)"
205
+ stackId="a"
206
+ />
207
+ <Bar
208
+ dataKey="errors"
209
+ fill="var(--color-errors)"
210
+ stackId="a"
211
+ radius={[2, 2, 0, 0]}
212
+ />
213
+ {markers(deploys)}
214
+ </BarChart>
215
+ </ChartContainer>
216
+ <Range data={data} />
217
+ </div>
218
+ )
219
+ }
220
+
221
+ // Release health's own vocabulary: sessions stacked by the worst status
222
+ // they reached, so a regression reads as amber and red eating the band.
223
+ const sessionConfig = {
224
+ ok: { label: "ok", color: "var(--ok)" },
225
+ errored: { label: "errored", color: "var(--warning)" },
226
+ crashed: { label: "crashed", color: "var(--danger)" },
227
+ } satisfies ChartConfig
228
+
229
+ export function SessionStatusChart({
230
+ data,
231
+ deploys,
232
+ className = "h-40",
233
+ }: {
234
+ data: SessionStatusPoint[]
235
+ deploys?: DeployMarker[]
236
+ className?: string
237
+ }) {
238
+ const hover = useBucketHover()
239
+ const tick = tickFormatter(data)
240
+ return (
241
+ <div>
242
+ <ChartContainer config={sessionConfig} className={`${className} w-full`}>
243
+ <AreaChart
244
+ data={data.map((d) => ({ ...d, t: new Date(d.t).toISOString() }))}
245
+ onMouseMove={hover.onMouseMove}
246
+ onMouseLeave={hover.onMouseLeave}
247
+ >
248
+ <CartesianGrid vertical={false} strokeDasharray="2 4" />
249
+ <XAxis
250
+ dataKey="t"
251
+ tickFormatter={tick}
252
+ tickLine={false}
253
+ axisLine={false}
254
+ minTickGap={40}
255
+ tick={axisTick}
256
+ />
257
+ <YAxis
258
+ width={36}
259
+ tickLine={false}
260
+ axisLine={false}
261
+ tick={axisTick}
262
+ tickFormatter={(v) => compact(Number(v))}
263
+ />
264
+ <ChartTooltip
265
+ content={
266
+ <ChartTooltipContent labelFormatter={(v) => stamp(String(v))} />
267
+ }
268
+ />
269
+ {(["ok", "errored", "crashed"] as const).map((key) => (
270
+ <Area
271
+ key={key}
272
+ dataKey={key}
273
+ stackId="sessions"
274
+ stroke={`var(--color-${key})`}
275
+ fill={`var(--color-${key})`}
276
+ fillOpacity={0.3}
277
+ strokeWidth={1.25}
278
+ isAnimationActive={false}
279
+ />
280
+ ))}
281
+ {markers(deploys)}
282
+ </AreaChart>
283
+ </ChartContainer>
284
+ <Range data={data} />
285
+ </div>
286
+ )
287
+ }
288
+
289
+ export function LatencyChart({
290
+ data,
291
+ deploys,
292
+ percentile = "p95",
293
+ className = "h-40",
294
+ }: {
295
+ data: SeriesPoint[]
296
+ deploys?: DeployMarker[]
297
+ percentile?: Percentile
298
+ className?: string
299
+ }) {
300
+ const hover = useBucketHover()
301
+ const tick = tickFormatter(data)
302
+ const points = data.map((d) => ({ ...d, t: new Date(d.t).toISOString() }))
303
+ return (
304
+ <div>
305
+ <ChartContainer config={config} className={`${className} w-full`}>
306
+ <LineChart
307
+ data={points}
308
+ onMouseMove={hover.onMouseMove}
309
+ onMouseLeave={hover.onMouseLeave}
310
+ >
311
+ <CartesianGrid vertical={false} strokeDasharray="2 4" />
312
+ <XAxis
313
+ dataKey="t"
314
+ tickFormatter={tick}
315
+ tickLine={false}
316
+ axisLine={false}
317
+ minTickGap={40}
318
+ tick={axisTick}
319
+ />
320
+ <YAxis
321
+ width={44}
322
+ tickLine={false}
323
+ axisLine={false}
324
+ tick={axisTick}
325
+ tickFormatter={(v) => ms(Number(v), 0)}
326
+ />
327
+ <ChartTooltip
328
+ content={
329
+ <ChartTooltipContent
330
+ labelFormatter={(v) => stamp(String(v))}
331
+ formatter={(value, name) => [
332
+ value == null ? "—" : ms(Number(value)),
333
+ String(name),
334
+ ]}
335
+ />
336
+ }
337
+ />
338
+ <Line
339
+ dataKey={percentile}
340
+ stroke={`var(--color-${percentile})`}
341
+ dot={loneDot(points, percentile, `var(--color-${percentile})`)}
342
+ strokeWidth={1.75}
343
+ isAnimationActive={false}
344
+ />
345
+ <Line
346
+ dataKey="avg"
347
+ stroke="var(--color-avg)"
348
+ dot={loneDot(points, "avg", "var(--color-avg)")}
349
+ strokeWidth={1.25}
350
+ strokeOpacity={0.8}
351
+ isAnimationActive={false}
352
+ />
353
+ {markers(deploys)}
354
+ </LineChart>
355
+ </ChartContainer>
356
+ <Range data={data} />
357
+ </div>
358
+ )
359
+ }
360
+
361
+ export function Sparkline({
362
+ data,
363
+ className = "h-10 w-32",
364
+ hoverKey,
365
+ }: {
366
+ data: number[]
367
+ className?: string
368
+ hoverKey?: string
369
+ }) {
370
+ const { hoveredKey, setHoveredKey } = useChartHover()
371
+ const active = hoverKey != null && hoveredKey === hoverKey
372
+ return (
373
+ <ChartContainer config={config} className={className}>
374
+ {/* recharts pads every side by 5px by default, which leaves a table
375
+ cell's 10px-tall sparkline no plot area at all; the stroke's
376
+ half-width is the only margin a chart this small needs. */}
377
+ <AreaChart
378
+ data={data.map((v) => ({ v }))}
379
+ margin={{ top: 1, right: 1, bottom: 1, left: 1 }}
380
+ onMouseEnter={hoverKey ? () => setHoveredKey(hoverKey) : undefined}
381
+ onMouseLeave={hoverKey ? () => setHoveredKey(null) : undefined}
382
+ >
383
+ <Area
384
+ dataKey="v"
385
+ stroke={active ? "var(--primary)" : "var(--muted-foreground)"}
386
+ fill={active ? "var(--primary)" : "var(--muted-foreground)"}
387
+ fillOpacity={active ? 0.35 : 0.15}
388
+ strokeWidth={active ? 2 : 1.25}
389
+ isAnimationActive={false}
390
+ />
391
+ </AreaChart>
392
+ </ChartContainer>
393
+ )
394
+ }
395
+
396
+ function compact(n: number) {
397
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
398
+ if (n >= 1_000) return `${(n / 1_000).toFixed(n >= 10_000 ? 0 : 1)}k`
399
+ return String(n)
400
+ }
@@ -0,0 +1,89 @@
1
+ import {
2
+ Dialog,
3
+ DialogContent,
4
+ DialogDescription,
5
+ DialogHeader,
6
+ DialogTitle,
7
+ } from "@/components/ui/dialog"
8
+
9
+ interface Shortcut {
10
+ keys: string[]
11
+ description: string
12
+ }
13
+
14
+ const GROUPS: { title: string; shortcuts: Shortcut[] }[] = [
15
+ {
16
+ title: "Navigation",
17
+ shortcuts: [
18
+ { keys: ["⌘", "K"], description: "Open command palette" },
19
+ { keys: ["g", "d"], description: "Go to dashboard" },
20
+ { keys: ["g", "o"], description: "Go to Overview" },
21
+ { keys: ["g", "r"], description: "Go to Requests" },
22
+ { keys: ["g", "j"], description: "Go to Jobs" },
23
+ { keys: ["g", "q"], description: "Go to Queries" },
24
+ { keys: ["g", "i"], description: "Go to Issues" },
25
+ { keys: ["?"], description: "Show this help" },
26
+ ],
27
+ },
28
+ {
29
+ title: "Lists (requests, jobs, queries, issues, logs, …)",
30
+ shortcuts: [
31
+ { keys: ["j"], description: "Move highlight down" },
32
+ { keys: ["k"], description: "Move highlight up" },
33
+ { keys: ["↵"], description: "Open highlighted row" },
34
+ { keys: ["o"], description: "Open highlighted row in a new tab" },
35
+ { keys: ["Esc"], description: "Clear highlight" },
36
+ { keys: ["/"], description: "Focus the filter bar" },
37
+ ],
38
+ },
39
+ ]
40
+
41
+ export function ShortcutsDialog({
42
+ open,
43
+ onOpenChange,
44
+ }: {
45
+ open: boolean
46
+ onOpenChange: (open: boolean) => void
47
+ }) {
48
+ return (
49
+ <Dialog open={open} onOpenChange={onOpenChange}>
50
+ <DialogContent className="sm:max-w-sm">
51
+ <DialogHeader>
52
+ <DialogTitle>Keyboard shortcuts</DialogTitle>
53
+ <DialogDescription>Available anywhere in the app.</DialogDescription>
54
+ </DialogHeader>
55
+ <div className="flex flex-col gap-4">
56
+ {GROUPS.map((group) => (
57
+ <div key={group.title} className="flex flex-col gap-2">
58
+ <div className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
59
+ {group.title}
60
+ </div>
61
+ <ul className="flex flex-col gap-2">
62
+ {group.shortcuts.map((shortcut) => (
63
+ <li
64
+ key={shortcut.description}
65
+ className="flex items-center justify-between text-sm"
66
+ >
67
+ <span className="text-muted-foreground">
68
+ {shortcut.description}
69
+ </span>
70
+ <span className="flex items-center gap-1">
71
+ {shortcut.keys.map((key, i) => (
72
+ <kbd
73
+ key={i}
74
+ className="bg-muted rounded border px-1.5 py-0.5 font-mono text-xs"
75
+ >
76
+ {key}
77
+ </kbd>
78
+ ))}
79
+ </span>
80
+ </li>
81
+ ))}
82
+ </ul>
83
+ </div>
84
+ ))}
85
+ </div>
86
+ </DialogContent>
87
+ </Dialog>
88
+ )
89
+ }
@@ -0,0 +1,121 @@
1
+ import { X } from "lucide-react"
2
+ import { toast } from "sonner"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ // Notices spoken in the signal vocabulary the rest of the UI uses. The
7
+ // aspect carries the meaning, so there is no icon to interpret: green is
8
+ // clear (done, saved, sent), amber is caution (something to look at, still
9
+ // safe), red is stop (this failed). The head sits at the left of the card
10
+ // like a lineside signal; the lit lamp is the only colour on the card.
11
+ export type ToastAspect = "clear" | "caution" | "stop"
12
+
13
+ const LAMP_CLASS: Record<ToastAspect, string> = {
14
+ stop: "bg-danger",
15
+ caution: "bg-warning",
16
+ clear: "bg-live",
17
+ }
18
+ const LAMP_ORDER: ToastAspect[] = ["stop", "caution", "clear"]
19
+
20
+ // Clear notices leave on their own; the line is clear and there is nothing
21
+ // to act on. Caution and stop wait to be read.
22
+ const DURATION: Record<ToastAspect, number> = {
23
+ clear: 4000,
24
+ caution: 8000,
25
+ stop: Infinity,
26
+ }
27
+
28
+ function Head({ aspect }: { aspect: ToastAspect }) {
29
+ return (
30
+ <span
31
+ aria-hidden
32
+ className="border-foreground/30 bg-background/60 inline-flex w-3 shrink-0 flex-col items-center justify-between rounded-full border px-px py-1"
33
+ style={{ height: 30 }}
34
+ >
35
+ {LAMP_ORDER.map((lamp) => (
36
+ <span key={lamp} className="relative inline-flex size-2">
37
+ {lamp === aspect && (
38
+ <span
39
+ className={cn(
40
+ "absolute inline-flex h-full w-full rounded-full opacity-40 blur-[2px]",
41
+ LAMP_CLASS[lamp],
42
+ )}
43
+ />
44
+ )}
45
+ <span
46
+ className={cn(
47
+ "relative inline-flex size-2 rounded-full",
48
+ lamp === aspect ? LAMP_CLASS[lamp] : "bg-neutral-400/15",
49
+ )}
50
+ />
51
+ </span>
52
+ ))}
53
+ </span>
54
+ )
55
+ }
56
+
57
+ // The card itself. `onDismiss` is what the X does; the marketing hero
58
+ // renders one with no dismiss as a static example.
59
+ export function SignalCard({
60
+ aspect,
61
+ title,
62
+ description,
63
+ onDismiss,
64
+ className,
65
+ }: {
66
+ aspect: ToastAspect
67
+ title: string
68
+ description?: string
69
+ onDismiss?: () => void
70
+ className?: string
71
+ }) {
72
+ const role = aspect === "stop" ? "alert" : "status"
73
+ return (
74
+ <div
75
+ role={onDismiss ? role : undefined}
76
+ data-aspect={aspect}
77
+ className={cn(
78
+ "bg-popover text-popover-foreground flex w-full items-start gap-3 rounded-lg border px-3.5 py-3 shadow-lg md:w-[360px]",
79
+ className,
80
+ )}
81
+ >
82
+ <Head aspect={aspect} />
83
+ <div className="min-w-0 flex-1 pt-0.5">
84
+ <p className="text-sm leading-snug font-medium">{title}</p>
85
+ {description && (
86
+ <p className="text-muted-foreground mt-0.5 text-xs leading-relaxed">
87
+ {description}
88
+ </p>
89
+ )}
90
+ </div>
91
+ {onDismiss && (
92
+ <button
93
+ type="button"
94
+ onClick={onDismiss}
95
+ aria-label="Dismiss"
96
+ className="text-muted-foreground hover:text-foreground -mt-0.5 -mr-1 rounded p-1"
97
+ >
98
+ <X className="size-3.5" />
99
+ </button>
100
+ )}
101
+ </div>
102
+ )
103
+ }
104
+
105
+ export function signalToast(
106
+ aspect: ToastAspect,
107
+ title: string,
108
+ description?: string,
109
+ ) {
110
+ return toast.custom(
111
+ (id) => (
112
+ <SignalCard
113
+ aspect={aspect}
114
+ title={title}
115
+ description={description}
116
+ onDismiss={() => toast.dismiss(id)}
117
+ />
118
+ ),
119
+ { duration: DURATION[aspect], unstyled: true },
120
+ )
121
+ }
@@ -0,0 +1,37 @@
1
+ import { ArrowDown, ArrowUp } from "lucide-react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ // Clickable DataTable column header for a sortable numeric column. Dumb by
6
+ // design: the page builds the sort/dir URL with its own route helper and
7
+ // passes the click handler in.
8
+ export function SortHeader({
9
+ label,
10
+ active,
11
+ dir,
12
+ onClick,
13
+ }: {
14
+ label: string
15
+ active: boolean
16
+ dir: "asc" | "desc"
17
+ onClick: () => void
18
+ }) {
19
+ return (
20
+ <button
21
+ type="button"
22
+ onClick={onClick}
23
+ className={cn(
24
+ "hover:text-foreground inline-flex items-center gap-1",
25
+ active && "text-foreground font-semibold",
26
+ )}
27
+ >
28
+ {label}
29
+ {active &&
30
+ (dir === "asc" ? (
31
+ <ArrowUp className="size-3" />
32
+ ) : (
33
+ <ArrowDown className="size-3" />
34
+ ))}
35
+ </button>
36
+ )
37
+ }