@shipfox/client-workflows 0.2.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 (327) hide show
  1. package/.storybook/main.ts +1 -0
  2. package/.storybook/preview.css +10 -0
  3. package/.storybook/preview.tsx +97 -0
  4. package/.swcrc +42 -0
  5. package/.turbo/turbo-build.log +2 -0
  6. package/.turbo/turbo-type$colon$emit.log +1 -0
  7. package/.turbo/turbo-type.log +1 -0
  8. package/CHANGELOG.md +162 -0
  9. package/LICENSE +21 -0
  10. package/dist/components/identifier/identifier.d.ts +6 -0
  11. package/dist/components/identifier/identifier.d.ts.map +1 -0
  12. package/dist/components/identifier/identifier.js +144 -0
  13. package/dist/components/identifier/identifier.js.map +1 -0
  14. package/dist/components/identifier/index.d.ts +2 -0
  15. package/dist/components/identifier/index.d.ts.map +1 -0
  16. package/dist/components/identifier/index.js +3 -0
  17. package/dist/components/identifier/index.js.map +1 -0
  18. package/dist/components/job-graph/graph-model.d.ts +27 -0
  19. package/dist/components/job-graph/graph-model.d.ts.map +1 -0
  20. package/dist/components/job-graph/graph-model.js +107 -0
  21. package/dist/components/job-graph/graph-model.js.map +1 -0
  22. package/dist/components/job-graph/index.d.ts +3 -0
  23. package/dist/components/job-graph/index.d.ts.map +1 -0
  24. package/dist/components/job-graph/index.js +3 -0
  25. package/dist/components/job-graph/index.js.map +1 -0
  26. package/dist/components/job-graph/job-duration-format.d.ts +4 -0
  27. package/dist/components/job-graph/job-duration-format.d.ts.map +1 -0
  28. package/dist/components/job-graph/job-duration-format.js +41 -0
  29. package/dist/components/job-graph/job-duration-format.js.map +1 -0
  30. package/dist/components/job-graph/job-duration-label.d.ts +5 -0
  31. package/dist/components/job-graph/job-duration-label.d.ts.map +1 -0
  32. package/dist/components/job-graph/job-duration-label.js +33 -0
  33. package/dist/components/job-graph/job-duration-label.js.map +1 -0
  34. package/dist/components/job-graph/job-graph-content.d.ts +9 -0
  35. package/dist/components/job-graph/job-graph-content.d.ts.map +1 -0
  36. package/dist/components/job-graph/job-graph-content.js +181 -0
  37. package/dist/components/job-graph/job-graph-content.js.map +1 -0
  38. package/dist/components/job-graph/job-graph-view.d.ts +11 -0
  39. package/dist/components/job-graph/job-graph-view.d.ts.map +1 -0
  40. package/dist/components/job-graph/job-graph-view.js +24 -0
  41. package/dist/components/job-graph/job-graph-view.js.map +1 -0
  42. package/dist/components/job-graph/job-graph.d.ts +3 -0
  43. package/dist/components/job-graph/job-graph.d.ts.map +1 -0
  44. package/dist/components/job-graph/job-graph.js +21 -0
  45. package/dist/components/job-graph/job-graph.js.map +1 -0
  46. package/dist/components/job-graph/job-node.d.ts +16 -0
  47. package/dist/components/job-graph/job-node.d.ts.map +1 -0
  48. package/dist/components/job-graph/job-node.js +196 -0
  49. package/dist/components/job-graph/job-node.js.map +1 -0
  50. package/dist/components/job-graph/types.d.ts +9 -0
  51. package/dist/components/job-graph/types.d.ts.map +1 -0
  52. package/dist/components/job-graph/types.js +3 -0
  53. package/dist/components/job-graph/types.js.map +1 -0
  54. package/dist/components/step-list/index.d.ts +3 -0
  55. package/dist/components/step-list/index.d.ts.map +1 -0
  56. package/dist/components/step-list/index.js +3 -0
  57. package/dist/components/step-list/index.js.map +1 -0
  58. package/dist/components/step-list/step-list-model.d.ts +36 -0
  59. package/dist/components/step-list/step-list-model.d.ts.map +1 -0
  60. package/dist/components/step-list/step-list-model.js +135 -0
  61. package/dist/components/step-list/step-list-model.js.map +1 -0
  62. package/dist/components/step-list/step-list.d.ts +32 -0
  63. package/dist/components/step-list/step-list.d.ts.map +1 -0
  64. package/dist/components/step-list/step-list.js +409 -0
  65. package/dist/components/step-list/step-list.js.map +1 -0
  66. package/dist/components/workflow-run-duration-label.d.ts +8 -0
  67. package/dist/components/workflow-run-duration-label.d.ts.map +1 -0
  68. package/dist/components/workflow-run-duration-label.js +84 -0
  69. package/dist/components/workflow-run-duration-label.js.map +1 -0
  70. package/dist/components/workflow-run-list/index.d.ts +2 -0
  71. package/dist/components/workflow-run-list/index.d.ts.map +1 -0
  72. package/dist/components/workflow-run-list/index.js +3 -0
  73. package/dist/components/workflow-run-list/index.js.map +1 -0
  74. package/dist/components/workflow-run-list/run-display.d.ts +5 -0
  75. package/dist/components/workflow-run-list/run-display.d.ts.map +1 -0
  76. package/dist/components/workflow-run-list/run-display.js +19 -0
  77. package/dist/components/workflow-run-list/run-display.js.map +1 -0
  78. package/dist/components/workflow-run-list/types.d.ts +21 -0
  79. package/dist/components/workflow-run-list/types.d.ts.map +1 -0
  80. package/dist/components/workflow-run-list/types.js +3 -0
  81. package/dist/components/workflow-run-list/types.js.map +1 -0
  82. package/dist/components/workflow-run-list/workflow-run-list-content.d.ts +14 -0
  83. package/dist/components/workflow-run-list/workflow-run-list-content.d.ts.map +1 -0
  84. package/dist/components/workflow-run-list/workflow-run-list-content.js +37 -0
  85. package/dist/components/workflow-run-list/workflow-run-list-content.js.map +1 -0
  86. package/dist/components/workflow-run-list/workflow-run-list-header.d.ts +10 -0
  87. package/dist/components/workflow-run-list/workflow-run-list-header.d.ts.map +1 -0
  88. package/dist/components/workflow-run-list/workflow-run-list-header.js +55 -0
  89. package/dist/components/workflow-run-list/workflow-run-list-header.js.map +1 -0
  90. package/dist/components/workflow-run-list/workflow-run-list-states.d.ts +15 -0
  91. package/dist/components/workflow-run-list/workflow-run-list-states.d.ts.map +1 -0
  92. package/dist/components/workflow-run-list/workflow-run-list-states.js +103 -0
  93. package/dist/components/workflow-run-list/workflow-run-list-states.js.map +1 -0
  94. package/dist/components/workflow-run-list/workflow-run-list-view.d.ts +3 -0
  95. package/dist/components/workflow-run-list/workflow-run-list-view.d.ts.map +1 -0
  96. package/dist/components/workflow-run-list/workflow-run-list-view.js +46 -0
  97. package/dist/components/workflow-run-list/workflow-run-list-view.js.map +1 -0
  98. package/dist/components/workflow-run-list/workflow-run-list.d.ts +3 -0
  99. package/dist/components/workflow-run-list/workflow-run-list.d.ts.map +1 -0
  100. package/dist/components/workflow-run-list/workflow-run-list.js +20 -0
  101. package/dist/components/workflow-run-list/workflow-run-list.js.map +1 -0
  102. package/dist/components/workflow-run-list/workflow-run-row.d.ts +14 -0
  103. package/dist/components/workflow-run-list/workflow-run-row.d.ts.map +1 -0
  104. package/dist/components/workflow-run-list/workflow-run-row.js +172 -0
  105. package/dist/components/workflow-run-list/workflow-run-row.js.map +1 -0
  106. package/dist/components/workflow-run-summary/index.d.ts +2 -0
  107. package/dist/components/workflow-run-summary/index.d.ts.map +1 -0
  108. package/dist/components/workflow-run-summary/index.js +3 -0
  109. package/dist/components/workflow-run-summary/index.js.map +1 -0
  110. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.d.ts +9 -0
  111. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.d.ts.map +1 -0
  112. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js +138 -0
  113. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js.map +1 -0
  114. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts +20 -0
  115. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts.map +1 -0
  116. package/dist/components/workflow-run-summary/workflow-run-summary.js +252 -0
  117. package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -0
  118. package/dist/components/workflow-run-view/agent-config-failure-callout.d.ts +7 -0
  119. package/dist/components/workflow-run-view/agent-config-failure-callout.d.ts.map +1 -0
  120. package/dist/components/workflow-run-view/agent-config-failure-callout.js +84 -0
  121. package/dist/components/workflow-run-view/agent-config-failure-callout.js.map +1 -0
  122. package/dist/components/workflow-run-view/index.d.ts +3 -0
  123. package/dist/components/workflow-run-view/index.d.ts.map +1 -0
  124. package/dist/components/workflow-run-view/index.js +4 -0
  125. package/dist/components/workflow-run-view/index.js.map +1 -0
  126. package/dist/components/workflow-run-view/job-card.d.ts +17 -0
  127. package/dist/components/workflow-run-view/job-card.d.ts.map +1 -0
  128. package/dist/components/workflow-run-view/job-card.js +476 -0
  129. package/dist/components/workflow-run-view/job-card.js.map +1 -0
  130. package/dist/components/workflow-run-view/job-execution-switcher.d.ts +10 -0
  131. package/dist/components/workflow-run-view/job-execution-switcher.d.ts.map +1 -0
  132. package/dist/components/workflow-run-view/job-execution-switcher.js +171 -0
  133. package/dist/components/workflow-run-view/job-execution-switcher.js.map +1 -0
  134. package/dist/components/workflow-run-view/job-execution-time-text.d.ts +6 -0
  135. package/dist/components/workflow-run-view/job-execution-time-text.d.ts.map +1 -0
  136. package/dist/components/workflow-run-view/job-execution-time-text.js +24 -0
  137. package/dist/components/workflow-run-view/job-execution-time-text.js.map +1 -0
  138. package/dist/components/workflow-run-view/step-attempt-log-panel.d.ts +9 -0
  139. package/dist/components/workflow-run-view/step-attempt-log-panel.d.ts.map +1 -0
  140. package/dist/components/workflow-run-view/step-attempt-log-panel.js +152 -0
  141. package/dist/components/workflow-run-view/step-attempt-log-panel.js.map +1 -0
  142. package/dist/components/workflow-run-view/workflow-run-selection.d.ts +14 -0
  143. package/dist/components/workflow-run-view/workflow-run-selection.d.ts.map +1 -0
  144. package/dist/components/workflow-run-view/workflow-run-selection.js +56 -0
  145. package/dist/components/workflow-run-view/workflow-run-selection.js.map +1 -0
  146. package/dist/components/workflow-run-view/workflow-run-states.d.ts +12 -0
  147. package/dist/components/workflow-run-view/workflow-run-states.d.ts.map +1 -0
  148. package/dist/components/workflow-run-view/workflow-run-states.js +91 -0
  149. package/dist/components/workflow-run-view/workflow-run-states.js.map +1 -0
  150. package/dist/components/workflow-run-view/workflow-run-view.d.ts +15 -0
  151. package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -0
  152. package/dist/components/workflow-run-view/workflow-run-view.js +285 -0
  153. package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -0
  154. package/dist/components/workflow-source-panel/index.d.ts +2 -0
  155. package/dist/components/workflow-source-panel/index.d.ts.map +1 -0
  156. package/dist/components/workflow-source-panel/index.js +3 -0
  157. package/dist/components/workflow-source-panel/index.js.map +1 -0
  158. package/dist/components/workflow-source-panel/workflow-source-panel.d.ts +13 -0
  159. package/dist/components/workflow-source-panel/workflow-source-panel.d.ts.map +1 -0
  160. package/dist/components/workflow-source-panel/workflow-source-panel.js +91 -0
  161. package/dist/components/workflow-source-panel/workflow-source-panel.js.map +1 -0
  162. package/dist/components/workflow-status/status-visuals.d.ts +12 -0
  163. package/dist/components/workflow-status/status-visuals.d.ts.map +1 -0
  164. package/dist/components/workflow-status/status-visuals.js +54 -0
  165. package/dist/components/workflow-status/status-visuals.js.map +1 -0
  166. package/dist/components/workflow-status/workflow-status-icon.d.ts +22 -0
  167. package/dist/components/workflow-status/workflow-status-icon.d.ts.map +1 -0
  168. package/dist/components/workflow-status/workflow-status-icon.js +100 -0
  169. package/dist/components/workflow-status/workflow-status-icon.js.map +1 -0
  170. package/dist/core/entities/job-execution.d.ts +69 -0
  171. package/dist/core/entities/job-execution.d.ts.map +1 -0
  172. package/dist/core/entities/job-execution.js +83 -0
  173. package/dist/core/entities/job-execution.js.map +1 -0
  174. package/dist/core/entities/job.d.ts +56 -0
  175. package/dist/core/entities/job.d.ts.map +1 -0
  176. package/dist/core/entities/job.js +71 -0
  177. package/dist/core/entities/job.js.map +1 -0
  178. package/dist/core/entities/step-attempt.d.ts +49 -0
  179. package/dist/core/entities/step-attempt.d.ts.map +1 -0
  180. package/dist/core/entities/step-attempt.js +48 -0
  181. package/dist/core/entities/step-attempt.js.map +1 -0
  182. package/dist/core/entities/step.d.ts +41 -0
  183. package/dist/core/entities/step.d.ts.map +1 -0
  184. package/dist/core/entities/step.js +51 -0
  185. package/dist/core/entities/step.js.map +1 -0
  186. package/dist/core/entities/workflow-run-attempt.d.ts +44 -0
  187. package/dist/core/entities/workflow-run-attempt.d.ts.map +1 -0
  188. package/dist/core/entities/workflow-run-attempt.js +51 -0
  189. package/dist/core/entities/workflow-run-attempt.js.map +1 -0
  190. package/dist/core/entities/workflow-run.d.ts +61 -0
  191. package/dist/core/entities/workflow-run.d.ts.map +1 -0
  192. package/dist/core/entities/workflow-run.js +104 -0
  193. package/dist/core/entities/workflow-run.js.map +1 -0
  194. package/dist/core/workflow-run-url-state.d.ts +13 -0
  195. package/dist/core/workflow-run-url-state.d.ts.map +1 -0
  196. package/dist/core/workflow-run-url-state.js +43 -0
  197. package/dist/core/workflow-run-url-state.js.map +1 -0
  198. package/dist/core/workflow-run.d.ts +13 -0
  199. package/dist/core/workflow-run.d.ts.map +1 -0
  200. package/dist/core/workflow-run.js +8 -0
  201. package/dist/core/workflow-run.js.map +1 -0
  202. package/dist/env.d.js +2 -0
  203. package/dist/env.d.js.map +1 -0
  204. package/dist/feature.d.ts +20 -0
  205. package/dist/feature.d.ts.map +1 -0
  206. package/dist/feature.js +27 -0
  207. package/dist/feature.js.map +1 -0
  208. package/dist/hooks/api/workflow-runs.d.ts +109 -0
  209. package/dist/hooks/api/workflow-runs.d.ts.map +1 -0
  210. package/dist/hooks/api/workflow-runs.js +394 -0
  211. package/dist/hooks/api/workflow-runs.js.map +1 -0
  212. package/dist/index.d.ts +5 -0
  213. package/dist/index.d.ts.map +1 -0
  214. package/dist/index.js +5 -0
  215. package/dist/index.js.map +1 -0
  216. package/dist/pages/workflow-run-first-time-use.d.ts +6 -0
  217. package/dist/pages/workflow-run-first-time-use.d.ts.map +1 -0
  218. package/dist/pages/workflow-run-first-time-use.js +17 -0
  219. package/dist/pages/workflow-run-first-time-use.js.map +1 -0
  220. package/dist/pages/workflow-run-page.d.ts +8 -0
  221. package/dist/pages/workflow-run-page.d.ts.map +1 -0
  222. package/dist/pages/workflow-run-page.js +83 -0
  223. package/dist/pages/workflow-run-page.js.map +1 -0
  224. package/dist/routes/run-detail.d.ts +8 -0
  225. package/dist/routes/run-detail.d.ts.map +1 -0
  226. package/dist/routes/run-detail.js +21 -0
  227. package/dist/routes/run-detail.js.map +1 -0
  228. package/dist/routes/runs.d.ts +8 -0
  229. package/dist/routes/runs.d.ts.map +1 -0
  230. package/dist/routes/runs.js +20 -0
  231. package/dist/routes/runs.js.map +1 -0
  232. package/dist/tsconfig.test.tsbuildinfo +1 -0
  233. package/package.json +114 -0
  234. package/src/components/identifier/identifier.tsx +178 -0
  235. package/src/components/identifier/index.ts +1 -0
  236. package/src/components/job-graph/graph-model.test.ts +189 -0
  237. package/src/components/job-graph/graph-model.ts +178 -0
  238. package/src/components/job-graph/index.ts +2 -0
  239. package/src/components/job-graph/job-duration-format.ts +54 -0
  240. package/src/components/job-graph/job-duration-label.tsx +29 -0
  241. package/src/components/job-graph/job-graph-content.tsx +225 -0
  242. package/src/components/job-graph/job-graph-view.test.tsx +298 -0
  243. package/src/components/job-graph/job-graph-view.tsx +45 -0
  244. package/src/components/job-graph/job-graph.stories.tsx +182 -0
  245. package/src/components/job-graph/job-graph.tsx +24 -0
  246. package/src/components/job-graph/job-node.stories.tsx +364 -0
  247. package/src/components/job-graph/job-node.test.tsx +347 -0
  248. package/src/components/job-graph/job-node.tsx +202 -0
  249. package/src/components/job-graph/types.ts +9 -0
  250. package/src/components/step-list/index.ts +6 -0
  251. package/src/components/step-list/step-list-model.test.ts +369 -0
  252. package/src/components/step-list/step-list-model.ts +205 -0
  253. package/src/components/step-list/step-list.stories.tsx +419 -0
  254. package/src/components/step-list/step-list.test.tsx +566 -0
  255. package/src/components/step-list/step-list.tsx +494 -0
  256. package/src/components/workflow-run-duration-label.tsx +118 -0
  257. package/src/components/workflow-run-list/index.ts +1 -0
  258. package/src/components/workflow-run-list/run-display.test.ts +53 -0
  259. package/src/components/workflow-run-list/run-display.ts +22 -0
  260. package/src/components/workflow-run-list/types.ts +22 -0
  261. package/src/components/workflow-run-list/workflow-run-list-content.tsx +58 -0
  262. package/src/components/workflow-run-list/workflow-run-list-header.tsx +53 -0
  263. package/src/components/workflow-run-list/workflow-run-list-states.tsx +86 -0
  264. package/src/components/workflow-run-list/workflow-run-list-view.test.tsx +178 -0
  265. package/src/components/workflow-run-list/workflow-run-list-view.tsx +57 -0
  266. package/src/components/workflow-run-list/workflow-run-list.stories.tsx +111 -0
  267. package/src/components/workflow-run-list/workflow-run-list.tsx +29 -0
  268. package/src/components/workflow-run-list/workflow-run-row.tsx +176 -0
  269. package/src/components/workflow-run-summary/index.ts +1 -0
  270. package/src/components/workflow-run-summary/workflow-run-attempt-switcher.test.tsx +230 -0
  271. package/src/components/workflow-run-summary/workflow-run-attempt-switcher.tsx +146 -0
  272. package/src/components/workflow-run-summary/workflow-run-summary.stories.tsx +414 -0
  273. package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +354 -0
  274. package/src/components/workflow-run-summary/workflow-run-summary.tsx +295 -0
  275. package/src/components/workflow-run-view/agent-config-failure-callout.stories.tsx +140 -0
  276. package/src/components/workflow-run-view/agent-config-failure-callout.tsx +93 -0
  277. package/src/components/workflow-run-view/index.ts +2 -0
  278. package/src/components/workflow-run-view/job-card.stories.tsx +788 -0
  279. package/src/components/workflow-run-view/job-card.tsx +566 -0
  280. package/src/components/workflow-run-view/job-execution-switcher.tsx +170 -0
  281. package/src/components/workflow-run-view/job-execution-time-text.tsx +34 -0
  282. package/src/components/workflow-run-view/step-attempt-log-panel.test.tsx +290 -0
  283. package/src/components/workflow-run-view/step-attempt-log-panel.tsx +152 -0
  284. package/src/components/workflow-run-view/workflow-run-selection.test.ts +280 -0
  285. package/src/components/workflow-run-view/workflow-run-selection.ts +89 -0
  286. package/src/components/workflow-run-view/workflow-run-states.tsx +70 -0
  287. package/src/components/workflow-run-view/workflow-run-view.test.tsx +1214 -0
  288. package/src/components/workflow-run-view/workflow-run-view.tsx +341 -0
  289. package/src/components/workflow-source-panel/index.ts +1 -0
  290. package/src/components/workflow-source-panel/workflow-source-panel.stories.tsx +110 -0
  291. package/src/components/workflow-source-panel/workflow-source-panel.test.tsx +96 -0
  292. package/src/components/workflow-source-panel/workflow-source-panel.tsx +136 -0
  293. package/src/components/workflow-status/status-visuals.test.ts +38 -0
  294. package/src/components/workflow-status/status-visuals.ts +36 -0
  295. package/src/components/workflow-status/workflow-status-icon.stories.tsx +90 -0
  296. package/src/components/workflow-status/workflow-status-icon.tsx +135 -0
  297. package/src/core/entities/job-execution.ts +139 -0
  298. package/src/core/entities/job.ts +149 -0
  299. package/src/core/entities/step-attempt.ts +89 -0
  300. package/src/core/entities/step.ts +105 -0
  301. package/src/core/entities/workflow-run-attempt.ts +87 -0
  302. package/src/core/entities/workflow-run.ts +183 -0
  303. package/src/core/workflow-run-url-state.ts +61 -0
  304. package/src/core/workflow-run.test.ts +640 -0
  305. package/src/core/workflow-run.ts +66 -0
  306. package/src/env.d.ts +8 -0
  307. package/src/feature.ts +26 -0
  308. package/src/hooks/api/workflow-runs.test.tsx +410 -0
  309. package/src/hooks/api/workflow-runs.ts +485 -0
  310. package/src/index.ts +45 -0
  311. package/src/page-harness-budget.test.ts +33 -0
  312. package/src/pages/workflow-run-first-time-use.tsx +17 -0
  313. package/src/pages/workflow-run-page.test.tsx +517 -0
  314. package/src/pages/workflow-run-page.tsx +89 -0
  315. package/src/routes/run-detail.tsx +15 -0
  316. package/src/routes/runs.tsx +11 -0
  317. package/test/fixtures/logs.ts +13 -0
  318. package/test/fixtures/workflow-run.ts +311 -0
  319. package/test/pages.tsx +79 -0
  320. package/test/render.tsx +40 -0
  321. package/test/setup.ts +3 -0
  322. package/tsconfig.build.json +9 -0
  323. package/tsconfig.build.tsbuildinfo +1 -0
  324. package/tsconfig.json +3 -0
  325. package/tsconfig.test.json +8 -0
  326. package/vercel.json +8 -0
  327. package/vitest.config.ts +70 -0
package/package.json ADDED
@@ -0,0 +1,114 @@
1
+ {
2
+ "name": "@shipfox/client-workflows",
3
+ "license": "MIT",
4
+ "version": "0.2.0",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
8
+ "directory": "libs/client/workflows"
9
+ },
10
+ "type": "module",
11
+ "main": "dist/index.js",
12
+ "types": "dist/index.d.ts",
13
+ "imports": {
14
+ "#*": {
15
+ "types": "./src/*",
16
+ "workspace-source": "./src/*",
17
+ "development": "./src/*",
18
+ "default": "./dist/*"
19
+ },
20
+ "#test/*": "./test/*"
21
+ },
22
+ "exports": {
23
+ ".": {
24
+ "development": {
25
+ "types": "./src/index.ts",
26
+ "default": "./src/index.ts"
27
+ },
28
+ "default": {
29
+ "types": "./dist/index.d.ts",
30
+ "default": "./dist/index.js"
31
+ }
32
+ },
33
+ "./feature": {
34
+ "development": {
35
+ "types": "./src/feature.ts",
36
+ "default": "./src/feature.ts"
37
+ },
38
+ "default": {
39
+ "types": "./dist/feature.d.ts",
40
+ "default": "./dist/feature.js"
41
+ }
42
+ },
43
+ "./routes/*": {
44
+ "development": {
45
+ "types": "./src/routes/*.tsx",
46
+ "default": "./src/routes/*.tsx"
47
+ },
48
+ "default": {
49
+ "types": "./dist/routes/*.d.ts",
50
+ "default": "./dist/routes/*.js"
51
+ }
52
+ }
53
+ },
54
+ "dependencies": {
55
+ "@swc/helpers": "^0.5.17",
56
+ "date-fns": "^4.1.0",
57
+ "@shipfox/api-workflows-dto": "2.0.0",
58
+ "@shipfox/client-logs": "0.2.0",
59
+ "@shipfox/client-shell": "0.2.0",
60
+ "@shipfox/client-api": "0.2.0",
61
+ "@shipfox/client-triggers": "0.2.0",
62
+ "@shipfox/client-ui": "0.2.0",
63
+ "@shipfox/react-ui": "0.3.1"
64
+ },
65
+ "peerDependencies": {
66
+ "@tanstack/react-query": "^5.101.0",
67
+ "@tanstack/react-router": "^1.170.16",
68
+ "react": "^19.0.0",
69
+ "react-dom": "^19.0.0"
70
+ },
71
+ "devDependencies": {
72
+ "@argos-ci/cli": "^5.0.0",
73
+ "@argos-ci/storybook": "^6.0.0",
74
+ "@storybook/addon-vitest": "^10.3.6",
75
+ "@storybook/react": "^10.0.0",
76
+ "@storybook/react-vite": "^10.0.0",
77
+ "@tailwindcss/vite": "^4.1.13",
78
+ "@tanstack/react-query": "^5.101.0",
79
+ "@tanstack/react-router": "^1.170.16",
80
+ "@testing-library/jest-dom": "^6.6.3",
81
+ "@testing-library/react": "^16.3.2",
82
+ "@testing-library/user-event": "^14.6.1",
83
+ "@types/react": "^19.1.11",
84
+ "@types/react-dom": "^19.1.7",
85
+ "@vitejs/plugin-react": "^6.0.0",
86
+ "@vitest/browser-playwright": "^4.1.5",
87
+ "happy-dom": "^20.8.3",
88
+ "jsdom": "^29.0.0",
89
+ "react": "^19.1.1",
90
+ "react-dom": "^19.1.1",
91
+ "storybook": "^10.0.0",
92
+ "storybook-addon-pseudo-states": "^10.0.0",
93
+ "tailwindcss": "^4.1.13",
94
+ "vite": "^8.0.3",
95
+ "vitest": "^4.1.5",
96
+ "@shipfox/swc": "1.2.5",
97
+ "@shipfox/ts-config": "1.3.8",
98
+ "@shipfox/typescript": "1.1.6",
99
+ "@shipfox/biome": "1.8.1",
100
+ "@shipfox/vitest": "1.2.2",
101
+ "@shipfox/client-test-setup": "0.0.2"
102
+ },
103
+ "scripts": {
104
+ "build": "shipfox-swc",
105
+ "check": "shipfox-biome-check",
106
+ "check:fix": "shipfox-biome-check --write",
107
+ "storybook": "storybook dev -p 6007",
108
+ "storybook:build": "storybook build -o storybook-static",
109
+ "test": "shipfox-vitest-run",
110
+ "test:watch": "shipfox-vitest-watch",
111
+ "type": "shipfox-tsc-check",
112
+ "type:emit": "shipfox-tsc-emit"
113
+ }
114
+ }
@@ -0,0 +1,178 @@
1
+ import {useCopyToClipboard} from '@shipfox/react-ui/hooks';
2
+ import {Icon} from '@shipfox/react-ui/icon';
3
+ import {Tooltip, TooltipContent, TooltipTrigger} from '@shipfox/react-ui/tooltip';
4
+ import {Code} from '@shipfox/react-ui/typography';
5
+ import {cn} from '@shipfox/react-ui/utils';
6
+ import {useCallback, useEffect, useLayoutEffect, useRef, useState} from 'react';
7
+ import {createPortal} from 'react-dom';
8
+
9
+ const COPY_FEEDBACK_OFFSET_PX = 8;
10
+
11
+ type CopyState = 'idle' | 'copied' | 'failed';
12
+
13
+ interface FeedbackAnchor {
14
+ bottom: number;
15
+ left: number;
16
+ top: number;
17
+ }
18
+
19
+ export function Identifier({
20
+ display,
21
+ value,
22
+ label = 'identifier',
23
+ }: {
24
+ display: string;
25
+ value: string;
26
+ label?: string | undefined;
27
+ }) {
28
+ const [copyState, setCopyState] = useState<CopyState>('idle');
29
+ const [tooltipOpen, setTooltipOpen] = useState(false);
30
+ const [feedbackAnchor, setFeedbackAnchor] = useState<FeedbackAnchor | null>(null);
31
+ const buttonRef = useRef<HTMLButtonElement | null>(null);
32
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
33
+ const {copy} = useCopyToClipboard({
34
+ text: value,
35
+ onCopy: () => {
36
+ setTemporaryCopyState('copied', 2500);
37
+ },
38
+ });
39
+
40
+ useEffect(() => {
41
+ return () => {
42
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
43
+ };
44
+ }, []);
45
+
46
+ const resetCopyFeedback = useCallback(() => {
47
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
48
+ setCopyState('idle');
49
+ setTooltipOpen(false);
50
+ setFeedbackAnchor(null);
51
+ }, []);
52
+
53
+ useEffect(() => {
54
+ if (copyState === 'idle') return;
55
+
56
+ window.addEventListener('scroll', resetCopyFeedback, true);
57
+ window.addEventListener('resize', resetCopyFeedback);
58
+
59
+ return () => {
60
+ window.removeEventListener('scroll', resetCopyFeedback, true);
61
+ window.removeEventListener('resize', resetCopyFeedback);
62
+ };
63
+ }, [copyState, resetCopyFeedback]);
64
+
65
+ function setTemporaryCopyState(state: Exclude<CopyState, 'idle'>, timeout: number) {
66
+ const rect = buttonRef.current?.getBoundingClientRect();
67
+ setCopyState(state);
68
+ setTooltipOpen(false);
69
+ setFeedbackAnchor(
70
+ rect
71
+ ? {
72
+ left: rect.left + rect.width / 2,
73
+ top: rect.top,
74
+ bottom: rect.bottom,
75
+ }
76
+ : null,
77
+ );
78
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
79
+ timeoutRef.current = setTimeout(resetCopyFeedback, timeout);
80
+ }
81
+
82
+ async function handleCopy() {
83
+ try {
84
+ await copy();
85
+ } catch {
86
+ setTemporaryCopyState('failed', 2500);
87
+ }
88
+ }
89
+
90
+ const ariaLabel =
91
+ copyState === 'copied'
92
+ ? `Copied ${label} ${value}`
93
+ : copyState === 'failed'
94
+ ? `Could not copy ${label} ${value}`
95
+ : `Copy ${label} ${value}`;
96
+
97
+ const feedback =
98
+ copyState === 'copied'
99
+ ? {kind: copyState, label: 'Copied'}
100
+ : copyState === 'failed'
101
+ ? {kind: copyState, label: 'Could not copy'}
102
+ : null;
103
+
104
+ return (
105
+ <>
106
+ <Tooltip open={copyState === 'idle' ? tooltipOpen : false} onOpenChange={setTooltipOpen}>
107
+ <TooltipTrigger asChild>
108
+ <button
109
+ ref={buttonRef}
110
+ type="button"
111
+ aria-label={ariaLabel}
112
+ onClick={() => {
113
+ void handleCopy();
114
+ }}
115
+ className="inline-flex h-20 min-w-0 shrink-0 items-center gap-4 rounded-4 px-2 text-foreground-neutral-muted outline-none transition-colors hover:bg-background-components-hover hover:text-foreground-neutral-base focus-visible:shadow-border-interactive-with-active"
116
+ >
117
+ <Icon name="copy" aria-hidden="true" className="size-12 shrink-0" />
118
+ <Code as="span" variant="label" className="truncate">
119
+ {display}
120
+ </Code>
121
+ </button>
122
+ </TooltipTrigger>
123
+ <TooltipContent>
124
+ <Code as="span" variant="label" className="block max-w-[360px] truncate">
125
+ {value}
126
+ </Code>
127
+ </TooltipContent>
128
+ </Tooltip>
129
+ {feedback && feedbackAnchor ? (
130
+ <CopyFeedbackPortal anchor={feedbackAnchor} kind={feedback.kind}>
131
+ {feedback.label}
132
+ </CopyFeedbackPortal>
133
+ ) : null}
134
+ </>
135
+ );
136
+ }
137
+
138
+ function CopyFeedbackPortal({
139
+ anchor,
140
+ children,
141
+ kind,
142
+ }: {
143
+ anchor: FeedbackAnchor;
144
+ children: string;
145
+ kind: Exclude<CopyState, 'idle'>;
146
+ }) {
147
+ const feedbackRef = useRef<HTMLSpanElement | null>(null);
148
+ const [placement, setPlacement] = useState<'top' | 'bottom'>('bottom');
149
+
150
+ useLayoutEffect(() => {
151
+ const height = feedbackRef.current?.offsetHeight ?? 0;
152
+ setPlacement(anchor.top >= height + COPY_FEEDBACK_OFFSET_PX ? 'top' : 'bottom');
153
+ }, [anchor.top]);
154
+
155
+ if (typeof document === 'undefined') return null;
156
+
157
+ return createPortal(
158
+ <span
159
+ ref={feedbackRef}
160
+ role="status"
161
+ style={{
162
+ left: anchor.left,
163
+ top: placement === 'top' ? anchor.top : anchor.bottom,
164
+ transform:
165
+ placement === 'top'
166
+ ? `translate(-50%, calc(-100% - ${COPY_FEEDBACK_OFFSET_PX}px))`
167
+ : `translate(-50%, ${COPY_FEEDBACK_OFFSET_PX}px)`,
168
+ }}
169
+ className={cn(
170
+ 'pointer-events-none fixed z-50 whitespace-nowrap rounded-8 bg-background-components-base px-8 py-4 text-xs font-display font-medium leading-20 shadow-tooltip',
171
+ kind === 'failed' ? 'text-foreground-highlight-error' : 'text-foreground-neutral-base',
172
+ )}
173
+ >
174
+ {children}
175
+ </span>,
176
+ document.body,
177
+ );
178
+ }
@@ -0,0 +1 @@
1
+ export {Identifier} from './identifier.js';
@@ -0,0 +1,189 @@
1
+ import type {WorkflowRunJobDetailDto} from '@shipfox/api-workflows-dto';
2
+ import type {Job, WorkflowRunDetail} from '#core/workflow-run.js';
3
+ import {workflowJob, workflowRunDetail} from '#test/fixtures/workflow-run.js';
4
+ import {buildJobGraphModel, nextJobGraphNodeId} from './graph-model.js';
5
+
6
+ describe('buildJobGraphModel', () => {
7
+ test('returns an empty model', () => {
8
+ const run = makeRun({jobs: []});
9
+
10
+ const result = buildJobGraphModel({run});
11
+
12
+ expect(result.nodes).toEqual([]);
13
+ expect(result.edges).toEqual([]);
14
+ expect(result.columns).toEqual([]);
15
+ });
16
+
17
+ test('maps a single job with a trigger edge', () => {
18
+ const run = makeRun({jobs: [makeJob({name: 'build'})]});
19
+
20
+ const result = buildJobGraphModel({run});
21
+
22
+ expect(result.nodes).toMatchObject([{name: 'build', column: 0, row: 0}]);
23
+ expect(result.edges).toMatchObject([
24
+ {from: 'trigger', to: result.nodes[0]?.id, kind: 'trigger'},
25
+ ]);
26
+ });
27
+
28
+ test('resolves dependencies by job key', () => {
29
+ const build = makeJob({name: 'build', position: 0});
30
+ const deploy = makeJob({name: 'deploy', position: 1, dependencies: ['build']});
31
+ const run = makeRun({jobs: [deploy, build]});
32
+
33
+ const result = buildJobGraphModel({run});
34
+
35
+ expect(nodeByName(result, 'build')).toMatchObject({column: 0});
36
+ expect(nodeByName(result, 'deploy')).toMatchObject({column: 1, dependencies: ['build']});
37
+ expect(result.edges).toContainEqual(
38
+ expect.objectContaining({from: build.id, to: deploy.id, kind: 'dependency'}),
39
+ );
40
+ });
41
+
42
+ test('emits one trigger edge for each root job in parallel fan-out', () => {
43
+ const jobs = Array.from({length: 10}, (_, index) =>
44
+ makeJob({name: `job-${String(index + 1).padStart(2, '0')}`, position: index}),
45
+ );
46
+ const run = makeRun({jobs});
47
+
48
+ const result = buildJobGraphModel({run});
49
+
50
+ expect(result.columns).toHaveLength(1);
51
+ expect(result.columns[0]).toHaveLength(10);
52
+ expect(result.edges.filter((edge) => edge.kind === 'trigger')).toHaveLength(10);
53
+ });
54
+
55
+ test('orders parallel jobs by position inside a column', () => {
56
+ const run = makeRun({
57
+ jobs: [
58
+ makeJob({name: 'zeta', position: 2}),
59
+ makeJob({name: 'alpha', position: 1}),
60
+ makeJob({name: 'middle', position: 3}),
61
+ ],
62
+ });
63
+
64
+ const result = buildJobGraphModel({run});
65
+
66
+ expect(result.columns[0]?.map((node) => node.name)).toEqual(['alpha', 'zeta', 'middle']);
67
+ });
68
+
69
+ test('lays out a ten-job sequence across ten columns', () => {
70
+ const jobs = Array.from({length: 10}, (_, index) =>
71
+ makeJob({
72
+ name: `job-${String(index + 1).padStart(2, '0')}`,
73
+ position: index,
74
+ dependencies: index === 0 ? [] : [`job-${String(index).padStart(2, '0')}`],
75
+ }),
76
+ );
77
+ const run = makeRun({jobs});
78
+
79
+ const result = buildJobGraphModel({run});
80
+
81
+ expect(result.columns).toHaveLength(10);
82
+ expect(nodeByName(result, 'job-10')).toMatchObject({column: 9});
83
+ });
84
+
85
+ test('places branch siblings in the same column and a join after them', () => {
86
+ const build = makeJob({name: 'build', position: 0});
87
+ const lint = makeJob({name: 'lint', position: 1, dependencies: ['build']});
88
+ const testJob = makeJob({name: 'test', position: 2, dependencies: ['build']});
89
+ const deploy = makeJob({name: 'deploy', position: 3, dependencies: ['lint', 'test']});
90
+ const run = makeRun({jobs: [deploy, testJob, lint, build]});
91
+
92
+ const result = buildJobGraphModel({run});
93
+
94
+ expect(nodeByName(result, 'lint')).toMatchObject({column: 1});
95
+ expect(nodeByName(result, 'test')).toMatchObject({column: 1});
96
+ expect(nodeByName(result, 'deploy')).toMatchObject({column: 2});
97
+ expect(nodeByName(result, 'deploy')?.dependencies).toEqual(['lint', 'test']);
98
+ });
99
+
100
+ test('moves keyboard navigation across columns and rows', () => {
101
+ const build = makeJob({name: 'build', position: 0});
102
+ const lint = makeJob({name: 'lint', position: 1, dependencies: ['build']});
103
+ const testJob = makeJob({name: 'test', position: 2, dependencies: ['build']});
104
+ const deploy = makeJob({name: 'deploy', position: 3, dependencies: ['lint', 'test']});
105
+ const run = makeRun({jobs: [build, lint, testJob, deploy]});
106
+ const model = buildJobGraphModel({run});
107
+
108
+ const nextFromBuild = nextJobGraphNodeId({
109
+ model,
110
+ currentNodeId: build.id,
111
+ key: 'ArrowRight',
112
+ });
113
+ const downFromLint = nextJobGraphNodeId({
114
+ model,
115
+ currentNodeId: lint.id,
116
+ key: 'ArrowDown',
117
+ });
118
+ const rightFromTest = nextJobGraphNodeId({
119
+ model,
120
+ currentNodeId: testJob.id,
121
+ key: 'ArrowRight',
122
+ });
123
+
124
+ expect(nextFromBuild).toBe(lint.id);
125
+ expect(downFromLint).toBe(testJob.id);
126
+ expect(rightFromTest).toBe(deploy.id);
127
+ });
128
+
129
+ test('keeps downstream skipped status as the persisted status', () => {
130
+ const build = makeJob({name: 'build', status: 'failed'});
131
+ const deploy = makeJob({
132
+ name: 'deploy',
133
+ status: 'skipped',
134
+ status_reason: 'dependency_not_completed',
135
+ position: 1,
136
+ dependencies: ['build'],
137
+ });
138
+ const run = makeRun({jobs: [build, deploy]});
139
+
140
+ const result = buildJobGraphModel({run});
141
+
142
+ expect(nodeByName(result, 'deploy')).toMatchObject({
143
+ status: 'skipped',
144
+ });
145
+ });
146
+
147
+ test('counts only pending and running known dependencies as current', () => {
148
+ const pending = makeJob({name: 'build', status: 'pending'});
149
+ const running = makeJob({name: 'test', status: 'running', position: 1});
150
+ const succeeded = makeJob({name: 'lint', status: 'succeeded', position: 2});
151
+ const failed = makeJob({name: 'security', status: 'failed', position: 3});
152
+ const cancelled = makeJob({name: 'docs', status: 'cancelled', position: 4});
153
+ const skipped = makeJob({name: 'deploy-preview', status: 'skipped', position: 5});
154
+ const deploy = makeJob({
155
+ name: 'deploy',
156
+ position: 6,
157
+ dependencies: ['build', 'test', 'lint', 'security', 'docs', 'deploy-preview', 'missing'],
158
+ });
159
+ const run = makeRun({jobs: [deploy, skipped, cancelled, failed, succeeded, running, pending]});
160
+
161
+ const result = buildJobGraphModel({run});
162
+
163
+ expect(nodeByName(result, 'deploy')).toMatchObject({
164
+ currentDependencyCount: 2,
165
+ });
166
+ });
167
+ });
168
+
169
+ function nodeByName(result: ReturnType<typeof buildJobGraphModel>, name: string) {
170
+ return result.nodes.find((node) => node.name === name);
171
+ }
172
+
173
+ function makeRun(overrides: Partial<WorkflowRunDetail> = {}): WorkflowRunDetail {
174
+ return {
175
+ ...workflowRunDetail({
176
+ name: 'Deploy',
177
+ trigger_provider: 'github',
178
+ trigger_source: 'github_acme',
179
+ trigger_event: 'push',
180
+ started_at: '2026-06-21T12:00:10.000Z',
181
+ jobs: [],
182
+ }),
183
+ ...overrides,
184
+ };
185
+ }
186
+
187
+ function makeJob(overrides: Partial<WorkflowRunJobDetailDto> & {name: string}): Job {
188
+ return workflowJob({key: overrides.key ?? overrides.name, ...overrides});
189
+ }
@@ -0,0 +1,178 @@
1
+ import type {Job, WorkflowRunDetail} from '#core/workflow-run.js';
2
+
3
+ export type JobGraphNode = Job & {
4
+ column: number;
5
+ row: number;
6
+ currentDependencyCount: number;
7
+ };
8
+
9
+ export type JobGraphNavigationKey =
10
+ | 'ArrowRight'
11
+ | 'ArrowLeft'
12
+ | 'ArrowDown'
13
+ | 'ArrowUp'
14
+ | 'j'
15
+ | 'k';
16
+
17
+ export interface JobGraphEdge {
18
+ id: string;
19
+ from: string;
20
+ to: string;
21
+ kind: 'trigger' | 'dependency';
22
+ }
23
+
24
+ export interface JobGraphModel {
25
+ nodes: JobGraphNode[];
26
+ edges: JobGraphEdge[];
27
+ columns: JobGraphNode[][];
28
+ }
29
+
30
+ export function buildJobGraphModel({run}: {run: WorkflowRunDetail}): JobGraphModel {
31
+ const sortedJobs = [...run.jobs].sort(compareJobs);
32
+ const byKey = new Map(sortedJobs.map((job) => [job.key, job]));
33
+ const columnMemo = new Map<string, number>();
34
+
35
+ function columnFor(job: Job, visiting = new Set<string>()): number {
36
+ const cached = columnMemo.get(job.id);
37
+ if (cached !== undefined) return cached;
38
+ if (visiting.has(job.id)) return 0;
39
+
40
+ const nextVisiting = new Set(visiting);
41
+ nextVisiting.add(job.id);
42
+
43
+ const dependencyColumns = job.dependencies
44
+ .map((dependencyKey) => byKey.get(dependencyKey))
45
+ .filter((dependency): dependency is Job => dependency !== undefined)
46
+ .map((dependency) => columnFor(dependency, nextVisiting));
47
+
48
+ const column = dependencyColumns.length === 0 ? 0 : Math.max(...dependencyColumns) + 1;
49
+ columnMemo.set(job.id, column);
50
+ return column;
51
+ }
52
+
53
+ const nodesWithoutRows = sortedJobs.map((job) =>
54
+ jobGraphNode(job, {
55
+ column: columnFor(job),
56
+ row: 0,
57
+ currentDependencyCount: currentDependencyCount(job, byKey),
58
+ }),
59
+ );
60
+
61
+ const grouped = groupColumns(nodesWithoutRows);
62
+ const nodes = grouped.flat();
63
+ const edges = buildEdges(sortedJobs, byKey);
64
+
65
+ return {
66
+ nodes,
67
+ edges,
68
+ columns: grouped,
69
+ };
70
+ }
71
+
72
+ function compareJobs(left: Job, right: Job): number {
73
+ return (
74
+ left.position - right.position ||
75
+ (left.name ?? left.key).localeCompare(right.name ?? right.key) ||
76
+ left.id.localeCompare(right.id)
77
+ );
78
+ }
79
+
80
+ function groupColumns(nodes: JobGraphNode[]): JobGraphNode[][] {
81
+ const byColumn = new Map<number, JobGraphNode[]>();
82
+ for (const node of nodes) {
83
+ const column = byColumn.get(node.column) ?? [];
84
+ column.push(node);
85
+ byColumn.set(node.column, column);
86
+ }
87
+
88
+ return [...byColumn.entries()]
89
+ .sort(([left], [right]) => left - right)
90
+ .map(([, column]) =>
91
+ column
92
+ .sort(
93
+ (left, right) =>
94
+ left.position - right.position ||
95
+ (left.name ?? left.key).localeCompare(right.name ?? right.key) ||
96
+ left.id.localeCompare(right.id),
97
+ )
98
+ .map((node, row) => jobGraphNode(node, {...node, row})),
99
+ );
100
+ }
101
+
102
+ function buildEdges(jobs: readonly Job[], byKey: ReadonlyMap<string, Job>): JobGraphEdge[] {
103
+ const triggerEdges = jobs
104
+ .filter((job) => job.dependencies.length === 0)
105
+ .map((job) => ({
106
+ id: `trigger:${job.id}`,
107
+ from: 'trigger',
108
+ to: job.id,
109
+ kind: 'trigger' as const,
110
+ }));
111
+
112
+ const dependencyEdges = jobs.flatMap((job) =>
113
+ job.dependencies.flatMap((dependencyKey) => {
114
+ const dependency = byKey.get(dependencyKey);
115
+ if (!dependency) return [];
116
+ return [
117
+ {
118
+ id: `${dependency.id}:${job.id}`,
119
+ from: dependency.id,
120
+ to: job.id,
121
+ kind: 'dependency' as const,
122
+ },
123
+ ];
124
+ }),
125
+ );
126
+
127
+ return [...triggerEdges, ...dependencyEdges];
128
+ }
129
+
130
+ function currentDependencyCount(job: Job, byKey: ReadonlyMap<string, Job>): number {
131
+ return job.dependencies.filter((dependencyKey) => {
132
+ const dependency = byKey.get(dependencyKey);
133
+ return dependency?.status === 'pending' || dependency?.status === 'running';
134
+ }).length;
135
+ }
136
+
137
+ function jobGraphNode(
138
+ job: Job,
139
+ layout: Pick<JobGraphNode, 'column' | 'row' | 'currentDependencyCount'>,
140
+ ): JobGraphNode {
141
+ return Object.assign(Object.create(Object.getPrototypeOf(job)), job, layout);
142
+ }
143
+
144
+ export function nextJobGraphNodeId({
145
+ model,
146
+ currentNodeId,
147
+ key,
148
+ }: {
149
+ model: JobGraphModel;
150
+ currentNodeId: string;
151
+ key: JobGraphNavigationKey;
152
+ }): string | undefined {
153
+ const current = model.nodes.find((node) => node.id === currentNodeId);
154
+ if (!current) return undefined;
155
+
156
+ switch (key) {
157
+ case 'ArrowRight':
158
+ return nodeInAdjacentColumn(model, current, 1)?.id;
159
+ case 'ArrowLeft':
160
+ return nodeInAdjacentColumn(model, current, -1)?.id;
161
+ case 'ArrowDown':
162
+ case 'j':
163
+ return model.columns[current.column]?.[current.row + 1]?.id;
164
+ case 'ArrowUp':
165
+ case 'k':
166
+ return model.columns[current.column]?.[current.row - 1]?.id;
167
+ }
168
+ }
169
+
170
+ function nodeInAdjacentColumn(
171
+ model: JobGraphModel,
172
+ current: JobGraphNode,
173
+ offset: -1 | 1,
174
+ ): JobGraphNode | undefined {
175
+ const column = model.columns[current.column + offset];
176
+ if (!column || column.length === 0) return undefined;
177
+ return column[Math.min(current.row, column.length - 1)];
178
+ }
@@ -0,0 +1,2 @@
1
+ export {JobGraph} from './job-graph.js';
2
+ export type {JobGraphProps} from './types.js';