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,312 @@
1
+ import { useEffect, useRef } from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ // A number whose digits roll when it changes: the digits that actually
6
+ // changed slide up (or down, when the number falls) to their new glyph and
7
+ // the rest of the number stays exactly where it is.
8
+ //
9
+ // This is motion on an operational dashboard, which is a thing to be
10
+ // careful with rather than a thing to sprinkle around -- see the gates on
11
+ // `Stat`'s rolling path, which is what decides whether this component is
12
+ // rendered at all. What this component owns is the part of the contract
13
+ // that no caller can get right by itself:
14
+ //
15
+ // * the roll is 300ms, so it is over long before the next update can
16
+ // arrive (see ROLL_MIN_INTERVAL_MS) and the strip is never in motion
17
+ // for more than ~15% of the time;
18
+ // * updates are coalesced to one roll every 2s. An update that lands
19
+ // inside that window is remembered, not painted, and the roll that
20
+ // follows goes straight to the newest value. A roll is never
21
+ // interrupted, because an animation that never finishes is worse than
22
+ // no animation at all;
23
+ // * the first paint is static. A page that opens, or one that is
24
+ // navigated back to, shows its numbers rather than animating up to
25
+ // them.
26
+ //
27
+ // The cells are built by hand rather than rendered, because they are keyed
28
+ // by place value and animated with the Web Animations API, and neither of
29
+ // those survives React re-rendering the digits underneath them. They are
30
+ // decoration -- whoever renders this is expected to hide it from assistive
31
+ // tech and put the number in a text node beside it (`Stat` does).
32
+
33
+ // The one switch. Flip this to false and every rolling number in the app --
34
+ // the dashboard's stats, and whatever the landing page does with the same
35
+ // component -- becomes a plain static number.
36
+ export const DIGIT_ROLL_ENABLED = true
37
+
38
+ export const ROLL_DURATION_MS = 300
39
+ export const ROLL_EASING = "cubic-bezier(.22, 1, .36, 1)"
40
+ export const ROLL_MIN_INTERVAL_MS = 2_000
41
+
42
+ // Fixed em heights on every piece, so a roll cannot change the line box and
43
+ // the strip below the number cannot be pushed around by it.
44
+ const CELL_CLASS =
45
+ "inline-block h-[1.15em] overflow-hidden leading-[1.15em] align-top"
46
+ const SEPARATOR_CLASS =
47
+ "inline-block h-[1.15em] overflow-visible leading-[1.15em] align-top text-center"
48
+ const STRIP_CLASS = "block will-change-transform"
49
+ const SLOT_CLASS = "block h-[1.15em] leading-[1.15em]"
50
+ const SUFFIX_CLASS = "inline-block h-[1.15em] leading-[1.15em]"
51
+
52
+ type Direction = -1 | 0 | 1
53
+
54
+ // A thousands separator (or a decimal point, or a minus sign) never rolls,
55
+ // so it is a plain glyph rather than a two-slot stack.
56
+ interface SeparatorCell {
57
+ separator: true
58
+ el: HTMLSpanElement
59
+ char: string | null
60
+ }
61
+
62
+ interface DigitCell {
63
+ separator: false
64
+ el: HTMLSpanElement
65
+ strip: HTMLSpanElement
66
+ top: HTMLSpanElement
67
+ bottom: HTMLSpanElement
68
+ animation: Animation | null
69
+ char: string | null
70
+ }
71
+
72
+ type Cell = SeparatorCell | DigitCell
73
+
74
+ function span(className: string) {
75
+ const el = document.createElement("span")
76
+ el.className = className
77
+ return el
78
+ }
79
+
80
+ // "412,908ms" -> { digits: "412,908", suffix: "ms" }. The unit is one glyph
81
+ // run at the end of the number that nothing ever moves.
82
+ function split(formatted: string) {
83
+ let end = formatted.length
84
+ while (end > 0 && !/[0-9]/.test(formatted[end - 1])) end--
85
+ return { digits: formatted.slice(0, end), suffix: formatted.slice(end) }
86
+ }
87
+
88
+ function makeCell(separator: boolean): Cell {
89
+ const el = span(separator ? SEPARATOR_CLASS : CELL_CLASS)
90
+ if (separator) return { separator: true, el, char: null }
91
+ const strip = span(STRIP_CLASS)
92
+ const top = span(SLOT_CLASS)
93
+ const bottom = span(SLOT_CLASS)
94
+ strip.append(top, bottom)
95
+ el.append(strip)
96
+ return {
97
+ separator: false,
98
+ el,
99
+ strip,
100
+ top,
101
+ bottom,
102
+ animation: null,
103
+ char: null,
104
+ }
105
+ }
106
+
107
+ // Cells are keyed by distance from the right of the number, so a digit keeps
108
+ // its identity -- its place value -- even when the number gains a digit:
109
+ // 999 -> 1,000 leaves keys 0,1,2 rolling 9 -> 0 and simply inserts key 3
110
+ // (the comma) and key 4 (the 1). No re-keying, no cascade, and the digits
111
+ // that did not change never move.
112
+ function createRoll(
113
+ host: HTMLElement,
114
+ { durationMs, easing }: { durationMs: number; easing: string },
115
+ ) {
116
+ const cells = new Map<number, Cell>()
117
+ let order = ""
118
+
119
+ // Collapse a cell back to the single digit that is showing. The glyph it
120
+ // rolled away from is out of view but still in the DOM, and a selection
121
+ // (or a copy) would otherwise pick it up.
122
+ function settle(cell: DigitCell, char: string) {
123
+ cell.animation?.cancel()
124
+ cell.animation = null
125
+ cell.top.textContent = char
126
+ cell.bottom.textContent = ""
127
+ cell.char = char
128
+ }
129
+
130
+ function roll(cell: DigitCell, char: string, direction: -1 | 1) {
131
+ // jsdom has no Web Animations API. Nothing to roll there: show the digit.
132
+ if (typeof cell.strip.animate !== "function") {
133
+ settle(cell, char)
134
+ return
135
+ }
136
+ // Cancel, re-fill the slots, and start the new animation in one task, so
137
+ // the browser never paints the intermediate state: an interrupted roll
138
+ // restarts from wherever it had got to, towards the new target.
139
+ cell.animation?.cancel()
140
+ const from = cell.char ?? char
141
+ if (direction > 0) {
142
+ cell.top.textContent = from
143
+ cell.bottom.textContent = char
144
+ } else {
145
+ cell.top.textContent = char
146
+ cell.bottom.textContent = from
147
+ }
148
+ const keyframes =
149
+ direction > 0
150
+ ? [{ transform: "translateY(0)" }, { transform: "translateY(-50%)" }]
151
+ : [{ transform: "translateY(-50%)" }, { transform: "translateY(0)" }]
152
+ const animation = cell.strip.animate(keyframes, {
153
+ duration: durationMs,
154
+ easing,
155
+ fill: "forwards",
156
+ })
157
+ cell.animation = animation
158
+ cell.char = char
159
+ animation.onfinish = () => {
160
+ if (cell.animation === animation) settle(cell, char)
161
+ }
162
+ }
163
+
164
+ function enter(cell: Cell) {
165
+ if (typeof cell.el.animate !== "function") return
166
+ cell.el.animate(
167
+ [
168
+ { opacity: 0, transform: "translateY(0.35em)" },
169
+ { opacity: 1, transform: "none" },
170
+ ],
171
+ { duration: durationMs, easing },
172
+ )
173
+ }
174
+
175
+ return {
176
+ paint(formatted: string, direction: Direction) {
177
+ const { digits, suffix } = split(formatted)
178
+ const chars = digits.split("")
179
+ const wanted = chars.map((char, index) => ({
180
+ key: chars.length - 1 - index,
181
+ char,
182
+ }))
183
+ const nextOrder = `${wanted.map((cell) => cell.key).join(",")}|${suffix}`
184
+ const first = order === ""
185
+
186
+ if (nextOrder !== order) {
187
+ // The key set changed (the number gained or lost a digit): re-lay the
188
+ // row, reusing every cell whose key survived.
189
+ const fragment = document.createDocumentFragment()
190
+ const keep = new Set<number>()
191
+ for (const { key, char } of wanted) {
192
+ keep.add(key)
193
+ const separator = !/[0-9]/.test(char)
194
+ let cell = cells.get(key)
195
+ if (cell?.separator !== separator) {
196
+ cell = makeCell(separator)
197
+ cells.set(key, cell)
198
+ if (!first && direction !== 0) enter(cell)
199
+ }
200
+ fragment.append(cell.el)
201
+ }
202
+ for (const key of [...cells.keys()])
203
+ if (!keep.has(key)) cells.delete(key)
204
+ host.replaceChildren(fragment)
205
+ if (suffix) {
206
+ const el = span(SUFFIX_CLASS)
207
+ el.textContent = suffix
208
+ host.append(el)
209
+ }
210
+ order = nextOrder
211
+ }
212
+
213
+ for (const { key, char } of wanted) {
214
+ const cell = cells.get(key)
215
+ if (!cell || cell.char === char) continue // this digit did not change
216
+ if (cell.separator) {
217
+ cell.el.textContent = char
218
+ cell.char = char
219
+ } else if (cell.char === null || direction === 0) {
220
+ settle(cell, char)
221
+ } else {
222
+ roll(cell, char, direction)
223
+ }
224
+ }
225
+ },
226
+
227
+ destroy() {
228
+ for (const cell of cells.values())
229
+ if (!cell.separator) cell.animation?.cancel()
230
+ },
231
+ }
232
+ }
233
+
234
+ export function DigitRoll({
235
+ value,
236
+ format,
237
+ className,
238
+ durationMs = ROLL_DURATION_MS,
239
+ easing = ROLL_EASING,
240
+ minIntervalMs = ROLL_MIN_INTERVAL_MS,
241
+ }: {
242
+ value: number
243
+ format: (value: number) => string
244
+ className?: string
245
+ durationMs?: number
246
+ easing?: string
247
+ minIntervalMs?: number
248
+ }) {
249
+ const hostRef = useRef<HTMLSpanElement>(null)
250
+ const rollRef = useRef<ReturnType<typeof createRoll> | null>(null)
251
+ // What the cells are showing, which is not `value` while an update is
252
+ // being held back by the coalescing window.
253
+ const shownRef = useRef(value)
254
+ const pendingRef = useRef<number | null>(null)
255
+ const lastRollAtRef = useRef(0)
256
+ const timerRef = useRef<number | undefined>(undefined)
257
+ const optionsRef = useRef({ format, durationMs, easing, minIntervalMs })
258
+ useEffect(() => {
259
+ optionsRef.current = { format, durationMs, easing, minIntervalMs }
260
+ })
261
+
262
+ useEffect(() => {
263
+ const host = hostRef.current
264
+ if (!host) return
265
+ const { format, durationMs, easing } = optionsRef.current
266
+ const roll = createRoll(host, { durationMs, easing })
267
+ rollRef.current = roll
268
+ // Never on mount: the first paint is the number, not a roll up to it.
269
+ roll.paint(format(shownRef.current), 0)
270
+ return () => {
271
+ clearTimeout(timerRef.current)
272
+ timerRef.current = undefined
273
+ pendingRef.current = null
274
+ rollRef.current = null
275
+ roll.destroy()
276
+ }
277
+ // Built once; every later value goes through the effect below.
278
+ }, [])
279
+
280
+ useEffect(() => {
281
+ const roll = rollRef.current
282
+ if (!roll || value === shownRef.current) return
283
+
284
+ const apply = (next: number) => {
285
+ lastRollAtRef.current = Date.now()
286
+ roll.paint(
287
+ optionsRef.current.format(next),
288
+ next >= shownRef.current ? 1 : -1,
289
+ )
290
+ shownRef.current = next
291
+ }
292
+
293
+ const elapsed = Date.now() - lastRollAtRef.current
294
+ const wait = optionsRef.current.minIntervalMs - elapsed
295
+ if (wait <= 0) {
296
+ apply(value)
297
+ return
298
+ }
299
+ // Inside the window: hold this value (replacing any other one being
300
+ // held) and let the timer already running land the newest of them. The
301
+ // roll in flight is left alone.
302
+ pendingRef.current = value
303
+ timerRef.current ??= window.setTimeout(() => {
304
+ timerRef.current = undefined
305
+ const next = pendingRef.current
306
+ pendingRef.current = null
307
+ if (next !== null && next !== shownRef.current) apply(next)
308
+ }, wait)
309
+ }, [value])
310
+
311
+ return <span ref={hostRef} className={cn(className)} />
312
+ }
@@ -0,0 +1,159 @@
1
+ import { type LucideIcon } from "lucide-react"
2
+ import { type ReactNode, useId } from "react"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ // A stretch of track behind the message: two rails and a run of ties. Two
7
+ // masks shape it. A horizontal one fades the ends so the line runs off the
8
+ // edge of the panel rather than stopping at it, and a radial one dissolves
9
+ // the middle so the message sits in a clear pocket while the rails stay
10
+ // legible on either side. The pocket is narrower below md so a phone card
11
+ // still shows a good run of track on each side of the message.
12
+ export function Track({ className }: { className?: string }) {
13
+ const id = useId()
14
+ return (
15
+ <svg
16
+ aria-hidden
17
+ className={cn(
18
+ "text-foreground/35 dark:text-foreground/40 pointer-events-none absolute inset-x-0 top-1/2 h-12 w-full -translate-y-1/2",
19
+ "[mask-composite:intersect]",
20
+ "[mask-image:linear-gradient(to_right,transparent,black_10%,black_90%,transparent),radial-gradient(ellipse_200px_160%_at_50%_50%,transparent_20%,black_100%)]",
21
+ "md:[mask-image:linear-gradient(to_right,transparent,black_10%,black_90%,transparent),radial-gradient(ellipse_300px_160%_at_50%_50%,transparent_35%,black_100%)]",
22
+ className,
23
+ )}
24
+ preserveAspectRatio="none"
25
+ >
26
+ <defs>
27
+ <pattern id={id} width="22" height="48" patternUnits="userSpaceOnUse">
28
+ <rect x="9" y="9" width="4" height="30" rx="1" fill="currentColor" />
29
+ </pattern>
30
+ </defs>
31
+ <rect width="100%" height="100%" fill={`url(#${id})`} />
32
+ <rect y="16" width="100%" height="2.5" fill="currentColor" />
33
+ <rect y="29.5" width="100%" height="2.5" fill="currentColor" />
34
+ </svg>
35
+ )
36
+ }
37
+
38
+ export type SignalAspect = "clear" | "caution" | "stop"
39
+
40
+ const ASPECT_CLASS: Record<SignalAspect, string> = {
41
+ clear: "text-live",
42
+ caution: "text-warning",
43
+ stop: "text-danger",
44
+ }
45
+
46
+ // Lamps in their real order on a three-aspect head: stop on top, caution in
47
+ // the middle, clear at the bottom.
48
+ const LAMP_Y: Record<SignalAspect, number> = { stop: 8, caution: 17, clear: 26 }
49
+ const LAMP_ORDER: SignalAspect[] = ["stop", "caution", "clear"]
50
+
51
+ // A lineside signal standing just past the pocket, on the right. The post
52
+ // and hood take the track's ink; the one lit aspect uses the status
53
+ // vocabulary the rest of the UI already speaks (green = clear, amber =
54
+ // caution, rose = stop). An empty table is a clear line, so that is the
55
+ // default; pages whose empty state is a filter miss pass "caution".
56
+ export function Signal({
57
+ aspect,
58
+ className,
59
+ }: {
60
+ aspect: SignalAspect
61
+ className?: string
62
+ }) {
63
+ return (
64
+ <svg
65
+ aria-hidden
66
+ viewBox="0 0 20 64"
67
+ className={cn(
68
+ "pointer-events-none absolute top-1/2 h-16 w-5 -translate-y-[calc(50%+8px)]",
69
+ "left-[calc(50%+100px)] md:left-[calc(50%+150px)]",
70
+ className,
71
+ )}
72
+ >
73
+ <g className="text-foreground/35 dark:text-foreground/40">
74
+ <rect x="9" y="33" width="2" height="26" rx="1" fill="currentColor" />
75
+ <rect x="5" y="58" width="10" height="2" rx="1" fill="currentColor" />
76
+ <rect
77
+ x="4.75"
78
+ y="1.75"
79
+ width="10.5"
80
+ height="31.5"
81
+ rx="5.25"
82
+ fill="none"
83
+ stroke="currentColor"
84
+ strokeWidth="1.5"
85
+ />
86
+ </g>
87
+ {LAMP_ORDER.map((lamp) =>
88
+ lamp === aspect ? (
89
+ <g key={lamp} className={ASPECT_CLASS[lamp]} data-lamp={lamp}>
90
+ <circle
91
+ cx="10"
92
+ cy={LAMP_Y[lamp]}
93
+ r="6"
94
+ fill="currentColor"
95
+ opacity="0.2"
96
+ className="blur-[2px]"
97
+ />
98
+ <circle cx="10" cy={LAMP_Y[lamp]} r="3" fill="currentColor" />
99
+ </g>
100
+ ) : (
101
+ <circle
102
+ key={lamp}
103
+ cx="10"
104
+ cy={LAMP_Y[lamp]}
105
+ r="3"
106
+ className="text-foreground/15"
107
+ fill="currentColor"
108
+ />
109
+ ),
110
+ )}
111
+ </svg>
112
+ )
113
+ }
114
+
115
+ // Empty state for tables and panels: the icon, title, optional description
116
+ // and action, set in the clear pocket of a track that runs behind them,
117
+ // with a signal beside the line showing the aspect.
118
+ export function EmptyState({
119
+ icon: Icon,
120
+ title,
121
+ description,
122
+ action,
123
+ signal = "clear",
124
+ className,
125
+ }: {
126
+ icon?: LucideIcon
127
+ title: ReactNode
128
+ description?: ReactNode
129
+ action?: ReactNode
130
+ signal?: SignalAspect
131
+ className?: string
132
+ }) {
133
+ return (
134
+ <div
135
+ className={cn(
136
+ "relative flex min-h-48 items-center justify-center overflow-hidden px-4 py-8 text-center",
137
+ className,
138
+ )}
139
+ aria-live="polite"
140
+ >
141
+ <Track />
142
+ <Signal aspect={signal} />
143
+ <div className="relative flex max-w-xs flex-col items-center gap-1.5">
144
+ {Icon && (
145
+ <span className="bg-muted text-muted-foreground mb-1 flex size-9 items-center justify-center rounded-md">
146
+ <Icon className="size-4" />
147
+ </span>
148
+ )}
149
+ <span className="text-sm font-medium">{title}</span>
150
+ {description && (
151
+ <span className="text-muted-foreground text-xs leading-relaxed">
152
+ {description}
153
+ </span>
154
+ )}
155
+ {action && <div className="mt-2">{action}</div>}
156
+ </div>
157
+ </div>
158
+ )
159
+ }
@@ -0,0 +1,130 @@
1
+ import { Link, usePage } from "@inertiajs/react"
2
+ import { Bookmark, ChevronRight } from "lucide-react"
3
+ import { useState } from "react"
4
+
5
+ import { isViewActive, samePage } from "@/components/railwatch/saved-views"
6
+ import {
7
+ Collapsible,
8
+ CollapsibleContent,
9
+ CollapsibleTrigger,
10
+ } from "@/components/ui/collapsible"
11
+ import {
12
+ SidebarGroup,
13
+ SidebarMenu,
14
+ SidebarMenuButton,
15
+ SidebarMenuItem,
16
+ SidebarMenuSub,
17
+ SidebarMenuSubButton,
18
+ SidebarMenuSubItem,
19
+ } from "@/components/ui/sidebar"
20
+ import { ENV_PAGE_GROUPS, envPages } from "@/lib/env-pages"
21
+ import { cn } from "@/lib/utils"
22
+ import type { SharedProps } from "@/types"
23
+
24
+ // Environment navigation in the sidebar: Overview at the top, then one
25
+ // collapsible section per page group (Activity, Monitoring, Settings). The
26
+ // section containing the current page starts open; the others start
27
+ // closed so the list stays short.
28
+ export function EnvNav() {
29
+ const page = usePage<SharedProps>()
30
+ const { environment } = page.props
31
+ const current = page.url.split("?")[0]
32
+ const [openOverride, setOpenOverride] = useState<
33
+ Record<string, boolean | undefined>
34
+ >({})
35
+ if (!environment) return null
36
+
37
+ const items = envPages(
38
+ environment.application_id,
39
+ environment.id,
40
+ page.props.window as string,
41
+ )
42
+ const pinnedViews = (page.props.saved_views ?? []).filter((v) => v.pinned)
43
+ const isCurrent = (href: string) => {
44
+ const path = href.split("?")[0]
45
+ return current === path || current.startsWith(path + "/")
46
+ }
47
+ const overview = items[0]
48
+
49
+ return (
50
+ <SidebarGroup className="p-0">
51
+ <SidebarMenu>
52
+ <SidebarMenuItem>
53
+ <SidebarMenuButton
54
+ asChild
55
+ isActive={current === overview.href.split("?")[0]}
56
+ tooltip={{ children: overview.title }}
57
+ >
58
+ <Link href={overview.href} prefetch>
59
+ {overview.icon && <overview.icon />}
60
+ <span>{overview.title}</span>
61
+ </Link>
62
+ </SidebarMenuButton>
63
+ </SidebarMenuItem>
64
+ {ENV_PAGE_GROUPS.map((group) => {
65
+ const groupItems = items.filter(
66
+ (i) => i.group === group.key && i !== overview,
67
+ )
68
+ const containsCurrent = groupItems.some((i) => isCurrent(i.href))
69
+ const open = openOverride[group.key] ?? containsCurrent
70
+ const GroupIcon = groupItems[0]?.icon
71
+ return (
72
+ <Collapsible
73
+ key={group.key}
74
+ open={open}
75
+ onOpenChange={(v) =>
76
+ setOpenOverride((o) => ({ ...o, [group.key]: v }))
77
+ }
78
+ className="group/collapsible"
79
+ >
80
+ <SidebarMenuItem>
81
+ <CollapsibleTrigger asChild>
82
+ <SidebarMenuButton
83
+ tooltip={{ children: group.title }}
84
+ className={cn(containsCurrent && "text-sidebar-foreground")}
85
+ >
86
+ {GroupIcon && <GroupIcon />}
87
+ <span>{group.title}</span>
88
+ <ChevronRight className="ml-auto size-3.5 opacity-60 transition-transform group-data-[state=open]/collapsible:rotate-90" />
89
+ </SidebarMenuButton>
90
+ </CollapsibleTrigger>
91
+ <CollapsibleContent>
92
+ <SidebarMenuSub className="mr-0 gap-0.5 pr-0">
93
+ {groupItems.map((item) => (
94
+ <SidebarMenuSubItem key={item.title}>
95
+ <SidebarMenuSubButton
96
+ asChild
97
+ isActive={isCurrent(item.href)}
98
+ >
99
+ <Link href={item.href} prefetch>
100
+ <span>{item.title}</span>
101
+ </Link>
102
+ </SidebarMenuSubButton>
103
+ {pinnedViews
104
+ .filter((view) => samePage(view.url, item.href))
105
+ .map((view) => (
106
+ <SidebarMenuSubButton
107
+ key={view.id}
108
+ asChild
109
+ size="sm"
110
+ className="pl-4"
111
+ isActive={isViewActive(page.url, view.url)}
112
+ >
113
+ <Link href={view.url}>
114
+ <Bookmark className="opacity-60" />
115
+ <span>{view.name}</span>
116
+ </Link>
117
+ </SidebarMenuSubButton>
118
+ ))}
119
+ </SidebarMenuSubItem>
120
+ ))}
121
+ </SidebarMenuSub>
122
+ </CollapsibleContent>
123
+ </SidebarMenuItem>
124
+ </Collapsible>
125
+ )
126
+ })}
127
+ </SidebarMenu>
128
+ </SidebarGroup>
129
+ )
130
+ }