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,405 @@
1
+ import { useMemo, useState } from "react"
2
+
3
+ import { SqlBlock } from "@/components/railwatch/sql-block"
4
+ import {
5
+ HoverCard,
6
+ HoverCardContent,
7
+ HoverCardTrigger,
8
+ } from "@/components/ui/hover-card"
9
+ import { ms, usd } from "@/lib/format"
10
+ import { cn } from "@/lib/utils"
11
+ import type { TimelineEntry } from "@/types"
12
+
13
+ const colors: Record<string, string> = {
14
+ query: "bg-sky-500",
15
+ transaction: "bg-sky-300",
16
+ cache_event: "bg-violet-500",
17
+ outgoing_request: "bg-orange-500",
18
+ mail: "bg-pink-500",
19
+ broadcast: "bg-teal-500",
20
+ enqueued_job: "bg-amber-500",
21
+ view_render: "bg-emerald-500",
22
+ storage_op: "bg-indigo-500",
23
+ exception: "bg-red-500",
24
+ log: "bg-neutral-400",
25
+ span: "bg-fuchsia-500",
26
+ attachment: "bg-lime-500",
27
+ llm_call: "bg-cyan-400",
28
+ }
29
+
30
+ const typeLabels: Record<string, string> = {
31
+ query: "Query",
32
+ cache_event: "Cache",
33
+ outgoing_request: "Outgoing",
34
+ mail: "Mail",
35
+ broadcast: "Broadcast",
36
+ log: "Log",
37
+ exception: "Exception",
38
+ view_render: "View",
39
+ storage_op: "Storage",
40
+ enqueued_job: "Job",
41
+ transaction: "Transaction",
42
+ span: "Span",
43
+ attachment: "Attachment",
44
+ llm_call: "LLM",
45
+ }
46
+
47
+ const stageOrder = [
48
+ "middleware_before",
49
+ "action",
50
+ "render",
51
+ "middleware_after",
52
+ "body",
53
+ ]
54
+
55
+ const stageLabels: Record<string, string> = {
56
+ middleware_before: "before middleware",
57
+ action: "action",
58
+ render: "render",
59
+ middleware_after: "after middleware",
60
+ body: "body",
61
+ }
62
+
63
+ // Candidate gridline intervals (ms); pick the smallest that keeps the axis
64
+ // under ~8 ticks for the current span.
65
+ const INTERVALS = [
66
+ 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 30000, 60000,
67
+ ]
68
+
69
+ function pickInterval(span: number) {
70
+ const target = span / 8
71
+ return INTERVALS.find((i) => i >= target) ?? INTERVALS[INTERVALS.length - 1]
72
+ }
73
+
74
+ function axisTicks(span: number, interval: number) {
75
+ const out: number[] = []
76
+ for (let t = 0; t <= span; t += interval) out.push(t)
77
+ return out
78
+ }
79
+
80
+ // Row/axis/stage-bar all share this grid so the ms axis and gridlines line
81
+ // up exactly with every bar regardless of content.
82
+ // Phones drop the type column and shrink the label so the bars keep room.
83
+ const GRID =
84
+ "grid grid-cols-[6rem_1fr_3.5rem] items-center gap-2 md:grid-cols-[5rem_13rem_1fr_4rem]"
85
+ const TYPE_COL = "hidden md:block"
86
+
87
+ function EntryBody({
88
+ entry,
89
+ showSource,
90
+ }: {
91
+ entry: TimelineEntry
92
+ showSource?: boolean
93
+ }) {
94
+ const d = entry.detail ?? {}
95
+ return (
96
+ <div className="space-y-1.5">
97
+ <div className="text-muted-foreground flex items-center gap-2 text-[10px] tracking-wide uppercase">
98
+ <span>{typeLabels[entry.type] ?? entry.type}</span>
99
+ {entry.stage && (
100
+ <span>· {stageLabels[entry.stage] ?? entry.stage}</span>
101
+ )}
102
+ </div>
103
+ {entry.type === "query" && entry.sql ? (
104
+ <SqlBlock sql={entry.sql} wrap className="text-[11px]" />
105
+ ) : entry.type === "cache_event" ? (
106
+ <div className="font-mono text-xs break-all">
107
+ {String(d.op ?? "")}{" "}
108
+ <span className="font-semibold">{String(d.key ?? "")}</span>
109
+ </div>
110
+ ) : entry.type === "outgoing_request" ? (
111
+ <div className="font-mono text-xs break-all">
112
+ {String(d.method ?? "")} {String(d.url ?? entry.label)}
113
+ {d.status != null && (
114
+ <span className="text-muted-foreground"> → {String(d.status)}</span>
115
+ )}
116
+ {showSource && typeof d.body === "string" && d.body !== "" && (
117
+ <pre className="bg-muted mt-1.5 max-h-48 overflow-auto rounded p-2 text-[10px] break-all whitespace-pre-wrap">
118
+ {d.body}
119
+ </pre>
120
+ )}
121
+ </div>
122
+ ) : entry.type === "llm_call" ? (
123
+ <div className="font-mono text-xs break-all">
124
+ {String(d.tool ?? entry.label)}
125
+ {d.input_tokens != null && (
126
+ <span className="text-muted-foreground">
127
+ {" "}
128
+ · {String(d.input_tokens)} in / {String(d.output_tokens ?? 0)} out
129
+ </span>
130
+ )}
131
+ {d.cost != null && (
132
+ <span className="text-muted-foreground">
133
+ {" "}
134
+ · {usd(Number(d.cost))}
135
+ </span>
136
+ )}
137
+ {d.step != null && (
138
+ <span className="text-muted-foreground">
139
+ {" "}
140
+ · {String(d.workflow ?? "")} / {String(d.step)}
141
+ </span>
142
+ )}
143
+ {d.error != null && (
144
+ <div className="text-destructive mt-1">{String(d.error)}</div>
145
+ )}
146
+ </div>
147
+ ) : entry.type === "mail" ? (
148
+ <div className="font-mono text-xs break-all">
149
+ {String(d.subject ?? entry.label)}
150
+ </div>
151
+ ) : entry.type === "log" ? (
152
+ <div className="font-mono text-xs break-words">
153
+ <span className="text-muted-foreground uppercase">
154
+ {String(d.level ?? "")}
155
+ </span>{" "}
156
+ {String(d.message ?? entry.label)}
157
+ </div>
158
+ ) : entry.type === "exception" ? (
159
+ <div className="text-destructive font-mono text-xs break-words">
160
+ {entry.label}
161
+ </div>
162
+ ) : (
163
+ <div className="font-mono text-xs break-words">{entry.label}</div>
164
+ )}
165
+ <div className="text-muted-foreground flex flex-wrap gap-x-3 text-[11px] tabular-nums">
166
+ <span>+{ms(entry.offset)}</span>
167
+ {entry.duration != null && <span>{ms(entry.duration)}</span>}
168
+ </div>
169
+ {showSource && entry.source && (
170
+ <div className="text-muted-foreground border-t pt-1 font-mono text-[11px] break-all">
171
+ {entry.source}
172
+ </div>
173
+ )}
174
+ </div>
175
+ )
176
+ }
177
+
178
+ function Gridlines({ ticks, span }: { ticks: number[]; span: number }) {
179
+ return (
180
+ <>
181
+ {ticks.map((t) => (
182
+ <div
183
+ key={t}
184
+ className="bg-border/60 pointer-events-none absolute top-0 h-full w-px"
185
+ style={{ left: `${(t / span) * 100}%` }}
186
+ />
187
+ ))}
188
+ </>
189
+ )
190
+ }
191
+
192
+ export function Timeline({
193
+ entries,
194
+ total,
195
+ stages,
196
+ }: {
197
+ entries: TimelineEntry[]
198
+ total: number
199
+ stages: Record<string, number>
200
+ }) {
201
+ const [hidden, setHidden] = useState<Set<string>>(new Set())
202
+ const [expanded, setExpanded] = useState<string | null>(null)
203
+
204
+ const span = Math.max(
205
+ total,
206
+ ...entries.map((e) => e.offset + (e.duration ?? 0)),
207
+ 1,
208
+ )
209
+ const interval = pickInterval(span)
210
+ const ticks = axisTicks(span, interval)
211
+
212
+ const counts = useMemo(() => {
213
+ const c = new Map<string, number>()
214
+ for (const e of entries) c.set(e.type, (c.get(e.type) ?? 0) + 1)
215
+ return c
216
+ }, [entries])
217
+
218
+ const visible = entries.filter((e) => !hidden.has(e.type))
219
+
220
+ function toggleType(type: string) {
221
+ setHidden((prev) => {
222
+ const next = new Set(prev)
223
+ if (next.has(type)) next.delete(type)
224
+ else next.add(type)
225
+ return next
226
+ })
227
+ }
228
+
229
+ const stageBars = stageOrder
230
+ .filter((s) => stages[s] !== undefined)
231
+ .reduce<{ name: string; start: number; width: number }[]>((acc, s) => {
232
+ const start =
233
+ acc.length > 0
234
+ ? acc[acc.length - 1].start + acc[acc.length - 1].width
235
+ : 0
236
+ acc.push({ name: s, start, width: stages[s] })
237
+ return acc
238
+ }, [])
239
+
240
+ return (
241
+ <div className="space-y-2">
242
+ {counts.size > 0 && (
243
+ <div className="flex flex-wrap items-center gap-1.5">
244
+ {[...counts.entries()].map(([type, n]) => (
245
+ <button
246
+ key={type}
247
+ type="button"
248
+ onClick={() => toggleType(type)}
249
+ className={cn(
250
+ "flex items-center gap-1.5 rounded-full border px-2 py-0.5 text-[10px] font-medium transition-colors",
251
+ hidden.has(type)
252
+ ? "text-muted-foreground opacity-50"
253
+ : "hover:bg-muted/60",
254
+ )}
255
+ >
256
+ <span
257
+ className={cn(
258
+ "size-1.5 rounded-full",
259
+ colors[type] ?? "bg-neutral-500",
260
+ )}
261
+ />
262
+ {typeLabels[type] ?? type}
263
+ <span className="text-muted-foreground tabular-nums">{n}</span>
264
+ </button>
265
+ ))}
266
+ </div>
267
+ )}
268
+
269
+ <div className={cn(GRID, "px-2 text-[10px]")}>
270
+ <div className={TYPE_COL} />
271
+ <div />
272
+ <div className="relative h-4 select-none">
273
+ {ticks.map((t) => (
274
+ <span
275
+ key={t}
276
+ className="text-muted-foreground absolute -translate-x-1/2 tabular-nums"
277
+ style={{ left: `${(t / span) * 100}%` }}
278
+ >
279
+ {ms(t, 0)}
280
+ </span>
281
+ ))}
282
+ </div>
283
+ <div />
284
+ </div>
285
+
286
+ <div className={cn(GRID, "px-2")}>
287
+ <div className={TYPE_COL} />
288
+ <div />
289
+ <div className="bg-muted/40 relative h-5 overflow-hidden rounded text-[10px]">
290
+ <Gridlines ticks={ticks} span={span} />
291
+ {stageBars.map((s) => (
292
+ <div
293
+ key={s.name}
294
+ className="border-background bg-primary/20 absolute top-0 flex h-full items-center overflow-hidden border-r px-1 whitespace-nowrap"
295
+ style={{
296
+ left: `${(s.start / span) * 100}%`,
297
+ width: `${(s.width / span) * 100}%`,
298
+ }}
299
+ title={`${stageLabels[s.name] ?? s.name} ${ms(s.width)}`}
300
+ >
301
+ {stageLabels[s.name] ?? s.name} {ms(s.width, 0)}
302
+ </div>
303
+ ))}
304
+ </div>
305
+ <div />
306
+ </div>
307
+
308
+ <div className="max-h-[32rem] overflow-y-auto rounded-lg border">
309
+ {entries.length === 0 && (
310
+ <div className="text-muted-foreground p-4 text-center text-sm">
311
+ No child events recorded.
312
+ </div>
313
+ )}
314
+ {entries.length > 0 && visible.length === 0 && (
315
+ <div className="text-muted-foreground p-4 text-center text-sm">
316
+ No events match the active filters.
317
+ </div>
318
+ )}
319
+ {visible.map((e) => {
320
+ const key = `${e.type}-${e.id}`
321
+ const isSlow = span > 0 && (e.duration ?? 0) / span > 0.1
322
+ const isException = e.type === "exception"
323
+ return (
324
+ <div
325
+ key={key}
326
+ className="border-b last:border-b-0"
327
+ style={{
328
+ contentVisibility: "auto",
329
+ containIntrinsicSize: "0 28px",
330
+ }}
331
+ >
332
+ <HoverCard openDelay={150}>
333
+ <HoverCardTrigger asChild>
334
+ <button
335
+ type="button"
336
+ onClick={() => setExpanded(expanded === key ? null : key)}
337
+ className={cn(
338
+ GRID,
339
+ "hover:bg-muted/40 w-full px-2 py-1 text-left text-xs",
340
+ )}
341
+ >
342
+ <span
343
+ className={cn(
344
+ "text-muted-foreground truncate font-mono",
345
+ TYPE_COL,
346
+ )}
347
+ >
348
+ {typeLabels[e.type] ?? e.type}
349
+ </span>
350
+ {e.type === "query" && e.sql ? (
351
+ <SqlBlock sql={e.sql} />
352
+ ) : (
353
+ <span className="truncate font-mono">{e.label}</span>
354
+ )}
355
+ <div className="bg-muted/30 relative h-3 rounded">
356
+ <Gridlines ticks={ticks} span={span} />
357
+ {isException ? (
358
+ <div
359
+ className="absolute top-1/2 size-2.5 -translate-x-1/2 -translate-y-1/2 rotate-45 bg-red-500"
360
+ style={{
361
+ left: `${Math.min(100, (e.offset / span) * 100)}%`,
362
+ }}
363
+ />
364
+ ) : (
365
+ <div
366
+ className={cn(
367
+ "absolute top-0 h-full rounded",
368
+ colors[e.type] ?? "bg-neutral-500",
369
+ isSlow
370
+ ? "ring-foreground/30 opacity-100 ring-1 ring-inset"
371
+ : "opacity-70",
372
+ )}
373
+ style={{
374
+ left: `${Math.min(100, (e.offset / span) * 100)}%`,
375
+ width: `${Math.max(0.3, ((e.duration ?? 0) / span) * 100)}%`,
376
+ }}
377
+ />
378
+ )}
379
+ </div>
380
+ <span
381
+ className={cn(
382
+ "text-muted-foreground text-right tabular-nums",
383
+ isSlow && "text-foreground font-semibold",
384
+ )}
385
+ >
386
+ {e.duration != null ? ms(e.duration) : "–"}
387
+ </span>
388
+ </button>
389
+ </HoverCardTrigger>
390
+ <HoverCardContent className="w-80" align="start">
391
+ <EntryBody entry={e} />
392
+ </HoverCardContent>
393
+ </HoverCard>
394
+ {expanded === key && (
395
+ <div className="bg-muted/20 border-t px-4 py-2">
396
+ <EntryBody entry={e} showSource />
397
+ </div>
398
+ )}
399
+ </div>
400
+ )
401
+ })}
402
+ </div>
403
+ </div>
404
+ )
405
+ }
@@ -0,0 +1,44 @@
1
+ import { cn } from "@/lib/utils"
2
+
3
+ // busy-out-of-max as a hairline bar plus the raw pair, for thread pools and
4
+ // connection pools. Same tone thresholds the Processes stat strip uses:
5
+ // amber from 70%, red from 90%.
6
+ export function UtilisationBar({
7
+ busy,
8
+ max,
9
+ className,
10
+ }: {
11
+ busy: number | null
12
+ max: number | null
13
+ className?: string
14
+ }) {
15
+ if (!max) return <span className="text-muted-foreground">–</span>
16
+ const ratio = Math.min(1, (busy ?? 0) / max)
17
+ return (
18
+ <span className={cn("flex items-center gap-1.5", className)}>
19
+ <span
20
+ role="meter"
21
+ aria-label="Utilisation"
22
+ aria-valuenow={busy ?? 0}
23
+ aria-valuemin={0}
24
+ aria-valuemax={max}
25
+ className="bg-muted-foreground/25 relative h-1.5 w-10 shrink-0 overflow-hidden rounded-full"
26
+ >
27
+ <span
28
+ className={cn(
29
+ "absolute inset-y-0 left-0 rounded-full",
30
+ ratio >= 0.9
31
+ ? "bg-danger"
32
+ : ratio >= 0.7
33
+ ? "bg-warning"
34
+ : "bg-live",
35
+ )}
36
+ style={{ width: `${Math.max(4, ratio * 100)}%` }}
37
+ />
38
+ </span>
39
+ <span className="font-mono tabular-nums">
40
+ {busy ?? 0}/{max}
41
+ </span>
42
+ </span>
43
+ )
44
+ }
@@ -0,0 +1,147 @@
1
+ import { CalendarClock } from "lucide-react"
2
+ import { useState } from "react"
3
+
4
+ import { Segmented, SegmentedItem } from "@/components/railwatch/segmented"
5
+ import { Button } from "@/components/ui/button"
6
+ import { Input } from "@/components/ui/input"
7
+ import { Label } from "@/components/ui/label"
8
+ import {
9
+ Popover,
10
+ PopoverContent,
11
+ PopoverTrigger,
12
+ } from "@/components/ui/popover"
13
+ import { WINDOWS, useWindow } from "@/hooks/use-window"
14
+
15
+ function toLocalInputValue(iso: string) {
16
+ const d = new Date(iso)
17
+ const pad = (n: number) => String(n).padStart(2, "0")
18
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`
19
+ }
20
+
21
+ function startOfDay(d: Date) {
22
+ const x = new Date(d)
23
+ x.setHours(0, 0, 0, 0)
24
+ return x
25
+ }
26
+
27
+ const PRESETS: { label: string; range: () => [Date, Date] }[] = [
28
+ { label: "Today", range: () => [startOfDay(new Date()), new Date()] },
29
+ {
30
+ label: "Yesterday",
31
+ range: () => {
32
+ const end = startOfDay(new Date())
33
+ const start = startOfDay(new Date(end.getTime() - 24 * 60 * 60 * 1000))
34
+ return [start, end]
35
+ },
36
+ },
37
+ {
38
+ label: "This week",
39
+ range: () => {
40
+ const start = startOfDay(new Date())
41
+ start.setDate(start.getDate() - start.getDay())
42
+ return [start, new Date()]
43
+ },
44
+ },
45
+ ]
46
+
47
+ // Fixed 1h–30d toggle plus a "Custom" popover with quick presets and
48
+ // from/to datetime-local inputs; both write through useWindow().
49
+ export function WindowPicker() {
50
+ const { window, from, to, set, setRange, label } = useWindow()
51
+ const [open, setOpen] = useState(false)
52
+ const [fromInput, setFromInput] = useState(() =>
53
+ toLocalInputValue(
54
+ from ?? new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
55
+ ),
56
+ )
57
+ const [toInput, setToInput] = useState(() =>
58
+ toLocalInputValue(to ?? new Date().toISOString()),
59
+ )
60
+
61
+ const applyRange = (f: Date, t: Date) => {
62
+ setFromInput(toLocalInputValue(f.toISOString()))
63
+ setToInput(toLocalInputValue(t.toISOString()))
64
+ setRange(f.toISOString(), t.toISOString())
65
+ setOpen(false)
66
+ }
67
+
68
+ const applyInputs = () => {
69
+ const f = new Date(fromInput)
70
+ const t = new Date(toInput)
71
+ if (!Number.isNaN(f.getTime()) && !Number.isNaN(t.getTime()))
72
+ applyRange(f, t)
73
+ }
74
+
75
+ return (
76
+ <Segmented>
77
+ {WINDOWS.map((w) => (
78
+ <SegmentedItem
79
+ key={w.value}
80
+ active={window === w.value}
81
+ onClick={() => set(w.value)}
82
+ aria-label={w.label}
83
+ >
84
+ {w.value}
85
+ </SegmentedItem>
86
+ ))}
87
+ <Popover open={open} onOpenChange={setOpen}>
88
+ <PopoverTrigger asChild>
89
+ <SegmentedItem
90
+ active={window === "custom"}
91
+ onClick={() => setOpen(true)}
92
+ aria-label="Custom range"
93
+ className="px-2"
94
+ >
95
+ <CalendarClock className="size-3" />
96
+ <span className="hidden sm:inline">
97
+ {window === "custom" ? label : "Custom"}
98
+ </span>
99
+ </SegmentedItem>
100
+ </PopoverTrigger>
101
+ <PopoverContent className="w-72" align="end">
102
+ <div className="flex flex-col gap-3">
103
+ <div className="flex flex-wrap gap-1">
104
+ {PRESETS.map((preset) => (
105
+ <Button
106
+ key={preset.label}
107
+ type="button"
108
+ variant="outline"
109
+ size="sm"
110
+ onClick={() => {
111
+ const [f, t] = preset.range()
112
+ applyRange(f, t)
113
+ }}
114
+ >
115
+ {preset.label}
116
+ </Button>
117
+ ))}
118
+ </div>
119
+ <div className="flex flex-col gap-2">
120
+ <Label htmlFor="window-picker-from" className="text-xs">
121
+ From
122
+ </Label>
123
+ <Input
124
+ id="window-picker-from"
125
+ type="datetime-local"
126
+ value={fromInput}
127
+ onChange={(e) => setFromInput(e.target.value)}
128
+ />
129
+ <Label htmlFor="window-picker-to" className="text-xs">
130
+ To
131
+ </Label>
132
+ <Input
133
+ id="window-picker-to"
134
+ type="datetime-local"
135
+ value={toInput}
136
+ onChange={(e) => setToInput(e.target.value)}
137
+ />
138
+ </div>
139
+ <Button type="button" size="sm" onClick={applyInputs}>
140
+ Apply
141
+ </Button>
142
+ </div>
143
+ </PopoverContent>
144
+ </Popover>
145
+ </Segmented>
146
+ )
147
+ }
@@ -0,0 +1,51 @@
1
+ import * as AvatarPrimitive from "@radix-ui/react-avatar"
2
+ import type * as React from "react"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ function Avatar({
7
+ className,
8
+ ...props
9
+ }: React.ComponentProps<typeof AvatarPrimitive.Root>) {
10
+ return (
11
+ <AvatarPrimitive.Root
12
+ data-slot="avatar"
13
+ className={cn(
14
+ "relative flex size-8 shrink-0 overflow-hidden rounded-full",
15
+ className,
16
+ )}
17
+ {...props}
18
+ />
19
+ )
20
+ }
21
+
22
+ function AvatarImage({
23
+ className,
24
+ ...props
25
+ }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
26
+ return (
27
+ <AvatarPrimitive.Image
28
+ data-slot="avatar-image"
29
+ className={cn("aspect-square size-full", className)}
30
+ {...props}
31
+ />
32
+ )
33
+ }
34
+
35
+ function AvatarFallback({
36
+ className,
37
+ ...props
38
+ }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
39
+ return (
40
+ <AvatarPrimitive.Fallback
41
+ data-slot="avatar-fallback"
42
+ className={cn(
43
+ "bg-muted flex size-full items-center justify-center rounded-full",
44
+ className,
45
+ )}
46
+ {...props}
47
+ />
48
+ )
49
+ }
50
+
51
+ export { Avatar, AvatarFallback, AvatarImage }
@@ -0,0 +1,46 @@
1
+ import { Slot } from "@radix-ui/react-slot"
2
+ import { type VariantProps, cva } from "class-variance-authority"
3
+ import type * as React from "react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const badgeVariants = cva(
8
+ "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default:
13
+ "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
14
+ secondary:
15
+ "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
16
+ destructive:
17
+ "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
18
+ outline:
19
+ "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
20
+ },
21
+ },
22
+ defaultVariants: {
23
+ variant: "default",
24
+ },
25
+ },
26
+ )
27
+
28
+ function Badge({
29
+ className,
30
+ variant,
31
+ asChild = false,
32
+ ...props
33
+ }: React.ComponentProps<"span"> &
34
+ VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
35
+ const Comp = asChild ? Slot : "span"
36
+
37
+ return (
38
+ <Comp
39
+ data-slot="badge"
40
+ className={cn(badgeVariants({ variant }), className)}
41
+ {...props}
42
+ />
43
+ )
44
+ }
45
+
46
+ export { Badge, badgeVariants }