@shipfox/client-workflows 16.0.0 → 17.0.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 (180) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-type.log +1 -0
  3. package/CHANGELOG.md +20 -0
  4. package/dist/components/identifier/identifier.js +2 -2
  5. package/dist/components/identifier/identifier.js.map +1 -1
  6. package/dist/components/job-detail/agent-config-failure-callout.d.ts.map +1 -1
  7. package/dist/components/job-detail/agent-config-failure-callout.js +1 -1
  8. package/dist/components/job-detail/agent-config-failure-callout.js.map +1 -1
  9. package/dist/components/job-detail/job-context-panel.d.ts +6 -0
  10. package/dist/components/job-detail/job-context-panel.d.ts.map +1 -0
  11. package/dist/components/job-detail/job-context-panel.js +224 -0
  12. package/dist/components/job-detail/job-context-panel.js.map +1 -0
  13. package/dist/components/job-detail/job-detail-header.d.ts +3 -1
  14. package/dist/components/job-detail/job-detail-header.d.ts.map +1 -1
  15. package/dist/components/job-detail/job-detail-header.js +54 -48
  16. package/dist/components/job-detail/job-detail-header.js.map +1 -1
  17. package/dist/components/job-detail/job-detail-view.d.ts +1 -1
  18. package/dist/components/job-detail/job-detail-view.d.ts.map +1 -1
  19. package/dist/components/job-detail/job-detail-view.js +80 -44
  20. package/dist/components/job-detail/job-detail-view.js.map +1 -1
  21. package/dist/components/job-detail/job-empty-states.d.ts.map +1 -1
  22. package/dist/components/job-detail/job-empty-states.js +19 -10
  23. package/dist/components/job-detail/job-empty-states.js.map +1 -1
  24. package/dist/components/job-detail/job-execution-switcher.js +4 -4
  25. package/dist/components/job-detail/job-execution-switcher.js.map +1 -1
  26. package/dist/components/job-detail/json-code.d.ts +14 -0
  27. package/dist/components/job-detail/json-code.d.ts.map +1 -0
  28. package/dist/components/job-detail/json-code.js +93 -0
  29. package/dist/components/job-detail/json-code.js.map +1 -0
  30. package/dist/components/job-detail/step-attempt-log-panel.d.ts.map +1 -1
  31. package/dist/components/job-detail/step-attempt-log-panel.js +9 -6
  32. package/dist/components/job-detail/step-attempt-log-panel.js.map +1 -1
  33. package/dist/components/job-detail/step-troubleshooting.d.ts +18 -0
  34. package/dist/components/job-detail/step-troubleshooting.d.ts.map +1 -0
  35. package/dist/components/job-detail/step-troubleshooting.js +493 -0
  36. package/dist/components/job-detail/step-troubleshooting.js.map +1 -0
  37. package/dist/components/job-graph/job-graph-content.d.ts.map +1 -1
  38. package/dist/components/job-graph/job-graph-content.js +5 -4
  39. package/dist/components/job-graph/job-graph-content.js.map +1 -1
  40. package/dist/components/job-graph/job-node.js +3 -3
  41. package/dist/components/job-graph/job-node.js.map +1 -1
  42. package/dist/components/step-list/index.d.ts +1 -0
  43. package/dist/components/step-list/index.d.ts.map +1 -1
  44. package/dist/components/step-list/index.js.map +1 -1
  45. package/dist/components/step-list/step-list-model.d.ts.map +1 -1
  46. package/dist/components/step-list/step-list-model.js +5 -0
  47. package/dist/components/step-list/step-list-model.js.map +1 -1
  48. package/dist/components/step-list/step-list.d.ts +5 -1
  49. package/dist/components/step-list/step-list.d.ts.map +1 -1
  50. package/dist/components/step-list/step-list.js +117 -82
  51. package/dist/components/step-list/step-list.js.map +1 -1
  52. package/dist/components/workflow-run-duration-label.js +1 -1
  53. package/dist/components/workflow-run-duration-label.js.map +1 -1
  54. package/dist/components/workflow-run-list/job-status-strip.js +6 -6
  55. package/dist/components/workflow-run-list/job-status-strip.js.map +1 -1
  56. package/dist/components/workflow-run-list/workflow-run-filter-menu.js +1 -1
  57. package/dist/components/workflow-run-list/workflow-run-filter-menu.js.map +1 -1
  58. package/dist/components/workflow-run-list/workflow-run-filters.js +4 -4
  59. package/dist/components/workflow-run-list/workflow-run-filters.js.map +1 -1
  60. package/dist/components/workflow-run-list/workflow-run-list-states.js +6 -6
  61. package/dist/components/workflow-run-list/workflow-run-list-states.js.map +1 -1
  62. package/dist/components/workflow-run-list/workflow-run-list-view.js +1 -1
  63. package/dist/components/workflow-run-list/workflow-run-list-view.js.map +1 -1
  64. package/dist/components/workflow-run-list/workflow-run-row.js +7 -7
  65. package/dist/components/workflow-run-list/workflow-run-row.js.map +1 -1
  66. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js +1 -1
  67. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js.map +1 -1
  68. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts +0 -1
  69. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts.map +1 -1
  70. package/dist/components/workflow-run-summary/workflow-run-summary.js +8 -13
  71. package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
  72. package/dist/components/workflow-run-tabs/index.d.ts +1 -1
  73. package/dist/components/workflow-run-tabs/index.d.ts.map +1 -1
  74. package/dist/components/workflow-run-tabs/index.js.map +1 -1
  75. package/dist/components/workflow-run-tabs/run-annotation-count-chip.js +2 -2
  76. package/dist/components/workflow-run-tabs/run-annotation-count-chip.js.map +1 -1
  77. package/dist/components/workflow-run-tabs/run-annotation-list.d.ts +9 -2
  78. package/dist/components/workflow-run-tabs/run-annotation-list.d.ts.map +1 -1
  79. package/dist/components/workflow-run-tabs/run-annotation-list.js +28 -19
  80. package/dist/components/workflow-run-tabs/run-annotation-list.js.map +1 -1
  81. package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts.map +1 -1
  82. package/dist/components/workflow-run-tabs/run-annotation-summary-line.js +5 -5
  83. package/dist/components/workflow-run-tabs/run-annotation-summary-line.js.map +1 -1
  84. package/dist/components/workflow-run-view/run-workspace-nav.js +7 -7
  85. package/dist/components/workflow-run-view/run-workspace-nav.js.map +1 -1
  86. package/dist/components/workflow-run-view/workflow-run-states.js +6 -6
  87. package/dist/components/workflow-run-view/workflow-run-states.js.map +1 -1
  88. package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -1
  89. package/dist/components/workflow-run-view/workflow-run-view.js +72 -12
  90. package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
  91. package/dist/core/entities/job-execution.d.ts +7 -0
  92. package/dist/core/entities/job-execution.d.ts.map +1 -1
  93. package/dist/core/entities/job-execution.js.map +1 -1
  94. package/dist/core/entities/job.d.ts +9 -0
  95. package/dist/core/entities/job.d.ts.map +1 -1
  96. package/dist/core/entities/job.js.map +1 -1
  97. package/dist/core/entities/step-attempt.d.ts +29 -0
  98. package/dist/core/entities/step-attempt.d.ts.map +1 -1
  99. package/dist/core/entities/step-attempt.js.map +1 -1
  100. package/dist/core/entities/step.d.ts +3 -1
  101. package/dist/core/entities/step.d.ts.map +1 -1
  102. package/dist/core/entities/step.js.map +1 -1
  103. package/dist/core/run-annotation.d.ts +6 -0
  104. package/dist/core/run-annotation.d.ts.map +1 -1
  105. package/dist/core/run-annotation.js.map +1 -1
  106. package/dist/core/workflow-run.d.ts +1 -1
  107. package/dist/core/workflow-run.d.ts.map +1 -1
  108. package/dist/core/workflow-run.js.map +1 -1
  109. package/dist/hooks/api/annotations.d.ts +63 -0
  110. package/dist/hooks/api/annotations.d.ts.map +1 -0
  111. package/dist/hooks/api/annotations.js +145 -0
  112. package/dist/hooks/api/annotations.js.map +1 -0
  113. package/dist/hooks/api/step-attempt-detail.d.ts +14 -0
  114. package/dist/hooks/api/step-attempt-detail.d.ts.map +1 -0
  115. package/dist/hooks/api/step-attempt-detail.js +43 -0
  116. package/dist/hooks/api/step-attempt-detail.js.map +1 -0
  117. package/dist/hooks/api/workflow-run-mapper.d.ts +9 -2
  118. package/dist/hooks/api/workflow-run-mapper.d.ts.map +1 -1
  119. package/dist/hooks/api/workflow-run-mapper.js +47 -0
  120. package/dist/hooks/api/workflow-run-mapper.js.map +1 -1
  121. package/dist/pages/project-workflows-page.js +19 -19
  122. package/dist/pages/project-workflows-page.js.map +1 -1
  123. package/dist/pages/workflow-run-list-page.js +1 -1
  124. package/dist/pages/workflow-run-list-page.js.map +1 -1
  125. package/dist/tsconfig.test.tsbuildinfo +1 -1
  126. package/package.json +10 -10
  127. package/src/components/identifier/identifier.tsx +2 -2
  128. package/src/components/job-detail/agent-config-failure-callout.tsx +5 -1
  129. package/src/components/job-detail/job-context-panel.test.tsx +72 -0
  130. package/src/components/job-detail/job-context-panel.tsx +237 -0
  131. package/src/components/job-detail/job-detail-header.tsx +52 -46
  132. package/src/components/job-detail/job-detail-view.stories.tsx +555 -53
  133. package/src/components/job-detail/job-detail-view.tsx +87 -45
  134. package/src/components/job-detail/job-empty-states.tsx +23 -10
  135. package/src/components/job-detail/job-execution-switcher.tsx +5 -5
  136. package/src/components/job-detail/json-code.tsx +120 -0
  137. package/src/components/job-detail/step-attempt-log-panel.test.tsx +1 -0
  138. package/src/components/job-detail/step-attempt-log-panel.tsx +17 -6
  139. package/src/components/job-detail/step-troubleshooting.stories.tsx +120 -0
  140. package/src/components/job-detail/step-troubleshooting.test.tsx +337 -0
  141. package/src/components/job-detail/step-troubleshooting.tsx +537 -0
  142. package/src/components/job-graph/job-graph-content.tsx +4 -3
  143. package/src/components/job-graph/job-node.tsx +3 -3
  144. package/src/components/step-list/index.ts +1 -0
  145. package/src/components/step-list/step-list-model.ts +5 -0
  146. package/src/components/step-list/step-list.test.tsx +75 -2
  147. package/src/components/step-list/step-list.tsx +73 -20
  148. package/src/components/workflow-run-duration-label.tsx +1 -1
  149. package/src/components/workflow-run-list/job-status-strip.tsx +6 -6
  150. package/src/components/workflow-run-list/workflow-run-filter-menu.tsx +1 -1
  151. package/src/components/workflow-run-list/workflow-run-filters.tsx +4 -4
  152. package/src/components/workflow-run-list/workflow-run-list-states.tsx +6 -6
  153. package/src/components/workflow-run-list/workflow-run-list-view.tsx +1 -1
  154. package/src/components/workflow-run-list/workflow-run-row.tsx +7 -7
  155. package/src/components/workflow-run-summary/workflow-run-attempt-switcher.tsx +1 -1
  156. package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +1 -1
  157. package/src/components/workflow-run-summary/workflow-run-summary.tsx +11 -11
  158. package/src/components/workflow-run-tabs/index.ts +5 -1
  159. package/src/components/workflow-run-tabs/run-annotation-count-chip.tsx +2 -2
  160. package/src/components/workflow-run-tabs/run-annotation-list.tsx +25 -10
  161. package/src/components/workflow-run-tabs/run-annotation-summary-line.tsx +5 -5
  162. package/src/components/workflow-run-view/run-workspace-nav.tsx +7 -7
  163. package/src/components/workflow-run-view/workflow-run-states.tsx +6 -6
  164. package/src/components/workflow-run-view/workflow-run-view.tsx +114 -14
  165. package/src/core/entities/job-execution.ts +7 -0
  166. package/src/core/entities/job.ts +9 -0
  167. package/src/core/entities/step-attempt.ts +33 -0
  168. package/src/core/entities/step.ts +3 -1
  169. package/src/core/run-annotation.ts +6 -0
  170. package/src/core/workflow-run.ts +4 -0
  171. package/src/hooks/api/annotations.test.ts +84 -0
  172. package/src/hooks/api/annotations.ts +240 -0
  173. package/src/hooks/api/step-attempt-detail.ts +67 -0
  174. package/src/hooks/api/workflow-run-mapper.ts +46 -0
  175. package/src/hooks/api/workflow-runs.test.tsx +61 -1
  176. package/src/pages/project-workflows-page.tsx +19 -19
  177. package/src/pages/workflow-job-detail-page.test.tsx +52 -1
  178. package/src/pages/workflow-run-list-page.tsx +1 -1
  179. package/test/fixtures/workflow-run.ts +7 -0
  180. package/tsconfig.build.tsbuildinfo +1 -1
@@ -92,6 +92,35 @@ describe('StepList', () => {
92
92
  ).toBeInTheDocument();
93
93
  });
94
94
 
95
+ test('opens the inspector without expanding the step log', async () => {
96
+ const user = userEvent.setup();
97
+ const attempt = makeAttempt({status: 'succeeded'});
98
+ const step = makeStep({name: 'deploy', attempts: [attempt]});
99
+
100
+ function InspectableStepList() {
101
+ const [inspectorOpenAttemptId, setInspectorOpenAttemptId] = useState<string | null>(null);
102
+
103
+ return (
104
+ <StepList
105
+ job={makeJob({steps: [step]})}
106
+ inspectorOpenAttemptId={inspectorOpenAttemptId}
107
+ onInspectorOpenChange={setInspectorOpenAttemptId}
108
+ renderExpandedStep={({attemptId}) => <Text size="sm">logs for {attemptId}</Text>}
109
+ renderInspector={(entry) => <Text size="sm">inspector for {entry.id}</Text>}
110
+ />
111
+ );
112
+ }
113
+
114
+ render(<InspectableStepList />);
115
+
116
+ const deploy = screen.getByRole('button', {name: 'deploy, Succeeded, attempt 1'});
117
+ await user.click(screen.getByRole('button', {name: 'Inspect deploy, attempt 1'}));
118
+
119
+ expect(deploy).toHaveAttribute('aria-expanded', 'false');
120
+ expect(screen.queryByText(`logs for ${attempt.id}`)).not.toBeInTheDocument();
121
+ expect(screen.getByText(`inspector for ${attempt.id}`)).toBeInTheDocument();
122
+ });
123
+
95
124
  test('keeps multiple expanded rows open', async () => {
96
125
  const user = userEvent.setup();
97
126
  const buildAttempt = makeAttempt({status: 'succeeded'});
@@ -180,8 +209,8 @@ describe('StepList', () => {
180
209
  await user.click(buildRow);
181
210
  await user.click(deployRow);
182
211
 
183
- expect(buildRow).not.toHaveClass('bg-background-components-hover');
184
- expect(deployRow).toHaveClass('bg-background-components-hover');
212
+ expect(buildRow.parentElement?.parentElement).not.toHaveClass('bg-background-components-hover');
213
+ expect(deployRow.parentElement?.parentElement).toHaveClass('bg-background-components-hover');
185
214
  });
186
215
 
187
216
  test('opens a default selected attempt', () => {
@@ -280,6 +309,50 @@ describe('StepList', () => {
280
309
  expect(screen.getByText(`logs for ${deployAttempt.id}`)).toBeInTheDocument();
281
310
  });
282
311
 
312
+ test('does not auto-open inspectors for failed landing attempts', async () => {
313
+ const user = userEvent.setup();
314
+ const firstAttempt = makeAttempt({id: 'failed-attempt-first', status: 'failed'});
315
+ const secondAttempt = makeAttempt({id: 'failed-attempt-second', status: 'failed'});
316
+ const firstJob = makeJob({
317
+ id: 'job-first',
318
+ steps: [makeStep({name: 'build', status: 'failed', attempts: [firstAttempt]})],
319
+ });
320
+ const secondJob = makeJob({
321
+ id: 'job-second',
322
+ steps: [makeStep({name: 'deploy', status: 'failed', attempts: [secondAttempt]})],
323
+ });
324
+
325
+ function InspectableStepList({job, defaultAttemptId}: {job: Job; defaultAttemptId: string}) {
326
+ const [inspectorOpenAttemptId, setInspectorOpenAttemptId] = useState<string | null>(null);
327
+
328
+ return (
329
+ <StepList
330
+ job={job}
331
+ defaultSelectedAttemptId={defaultAttemptId}
332
+ inspectorOpenAttemptId={inspectorOpenAttemptId}
333
+ onInspectorOpenChange={setInspectorOpenAttemptId}
334
+ renderExpandedStep={({attemptId}) => <Text size="sm">logs for {attemptId}</Text>}
335
+ renderInspector={(entry) => <Text size="sm">inspector open {entry.id}</Text>}
336
+ />
337
+ );
338
+ }
339
+
340
+ const {rerender} = render(
341
+ <InspectableStepList job={firstJob} defaultAttemptId={firstAttempt.id} />,
342
+ );
343
+
344
+ expect(screen.queryByText(`inspector open ${firstAttempt.id}`)).not.toBeInTheDocument();
345
+
346
+ await user.click(
347
+ screen.getByRole('button', {name: `Inspect build, attempt ${firstAttempt.attempt}`}),
348
+ );
349
+ expect(screen.getByText(`inspector open ${firstAttempt.id}`)).toBeInTheDocument();
350
+
351
+ rerender(<InspectableStepList job={secondJob} defaultAttemptId={secondAttempt.id} />);
352
+
353
+ expect(screen.queryByText(`inspector open ${secondAttempt.id}`)).not.toBeInTheDocument();
354
+ });
355
+
283
356
  test('does not replace the landing selection when a live job reports a new candidate', () => {
284
357
  const firstAttempt = makeAttempt({status: 'running'});
285
358
  const secondAttempt = makeAttempt({status: 'running'});
@@ -58,9 +58,12 @@ export interface StepListProps {
58
58
  selectedAttemptId?: string | null | undefined;
59
59
  defaultSelectedAttemptId?: string | undefined;
60
60
  onSelectedAttemptChange?: ((attemptId: string | undefined) => void) | undefined;
61
+ inspectorOpenAttemptId?: string | null | undefined;
62
+ onInspectorOpenChange?: ((attemptId: string | null) => void) | undefined;
61
63
  autoSelectActiveAttempt?: boolean | undefined;
62
64
  emptyState?: StepListEmptyState | undefined;
63
65
  renderExpandedStep?: ((context: StepExpandedContext) => ReactNode) | undefined;
66
+ renderInspector?: ((entry: StepListEntryModel) => ReactNode) | undefined;
64
67
  showHeader?: boolean | undefined;
65
68
  className?: string | undefined;
66
69
  }
@@ -71,9 +74,12 @@ export function StepList({
71
74
  selectedAttemptId,
72
75
  defaultSelectedAttemptId,
73
76
  onSelectedAttemptChange,
77
+ inspectorOpenAttemptId = null,
78
+ onInspectorOpenChange,
74
79
  autoSelectActiveAttempt = false,
75
80
  emptyState,
76
81
  renderExpandedStep,
82
+ renderInspector,
77
83
  showHeader = true,
78
84
  className,
79
85
  }: StepListProps) {
@@ -90,9 +96,12 @@ export function StepList({
90
96
  selectedAttemptId={selectedAttemptId}
91
97
  defaultSelectedAttemptId={defaultSelectedAttemptId}
92
98
  onSelectedAttemptChange={onSelectedAttemptChange}
99
+ inspectorOpenAttemptId={inspectorOpenAttemptId}
100
+ onInspectorOpenChange={onInspectorOpenChange}
93
101
  autoSelectActiveAttempt={autoSelectActiveAttempt}
94
102
  emptyState={emptyState}
95
103
  renderExpandedStep={renderExpandedStep}
104
+ renderInspector={renderInspector}
96
105
  showHeader={showHeader}
97
106
  className={className}
98
107
  />
@@ -104,9 +113,12 @@ function StepListContent({
104
113
  selectedAttemptId,
105
114
  defaultSelectedAttemptId,
106
115
  onSelectedAttemptChange,
116
+ inspectorOpenAttemptId = null,
117
+ onInspectorOpenChange,
107
118
  autoSelectActiveAttempt,
108
119
  emptyState,
109
120
  renderExpandedStep,
121
+ renderInspector,
110
122
  showHeader,
111
123
  className,
112
124
  }: Omit<StepListProps, 'job' | 'jobExecution'> & {model: StepListModel}) {
@@ -185,6 +197,15 @@ function StepListContent({
185
197
  setUserSelectedAttempt(true);
186
198
  }, [selectedAttemptId]);
187
199
 
200
+ useEffect(() => {
201
+ if (
202
+ inspectorOpenAttemptId !== null &&
203
+ !model.entries.some((entry) => entry.id === inspectorOpenAttemptId && !entry.carriedOver)
204
+ ) {
205
+ onInspectorOpenChange?.(null);
206
+ }
207
+ }, [inspectorOpenAttemptId, model.entries, onInspectorOpenChange]);
208
+
188
209
  function selectAttempt(nextAttemptIds: string[]) {
189
210
  const nextAttemptId = nextSelectedAttemptId(selectedAttemptIds, nextAttemptIds);
190
211
  setUserSelectedAttempt(true);
@@ -193,6 +214,10 @@ function StepListContent({
193
214
  onSelectedAttemptChange?.(nextAttemptId);
194
215
  }
195
216
 
217
+ const inspectorEntry = model.entries.find(
218
+ (entry) => entry.id === inspectorOpenAttemptId && !entry.carriedOver,
219
+ );
220
+
196
221
  return (
197
222
  <TimeTickerProvider intervalMs={1000} reducedMotionIntervalMs={10_000}>
198
223
  <section
@@ -203,7 +228,7 @@ function StepListContent({
203
228
  )}
204
229
  >
205
230
  {showHeader ? (
206
- <div className="flex min-h-40 items-center border-b border-border-neutral-base px-16 py-8">
231
+ <div className="flex min-h-40 items-center border-b border-border-neutral-base px-row py-row">
207
232
  <Text as="h2" id={titleId} size="sm" bold className="text-foreground-neutral-base">
208
233
  {model.jobName}
209
234
  </Text>
@@ -220,7 +245,7 @@ function StepListContent({
220
245
  asChild
221
246
  >
222
247
  <ol>
223
- {model.entries.map((entry) => {
248
+ {model.entries.map((entry, index) => {
224
249
  const selected = selectedAttemptIds.includes(entry.id);
225
250
  return (
226
251
  <StepRow
@@ -228,6 +253,7 @@ function StepListContent({
228
253
  entry={entry}
229
254
  selected={selected}
230
255
  hasExpandedContent={hasExpandedContent}
256
+ isLast={index === model.entries.length - 1}
231
257
  onSelect={() => {
232
258
  selectAttempt(
233
259
  hasExpandedContent
@@ -237,6 +263,11 @@ function StepListContent({
237
263
  : [entry.id],
238
264
  );
239
265
  }}
266
+ onInspect={
267
+ onInspectorOpenChange && !entry.carriedOver
268
+ ? () => onInspectorOpenChange(entry.id)
269
+ : undefined
270
+ }
240
271
  expandedContent={
241
272
  selected
242
273
  ? renderExpandedStep?.({
@@ -260,6 +291,7 @@ function StepListContent({
260
291
  </Accordion>
261
292
  )}
262
293
  </section>
294
+ {inspectorEntry ? renderInspector?.(inspectorEntry) : null}
263
295
  </TimeTickerProvider>
264
296
  );
265
297
  }
@@ -275,7 +307,7 @@ function StepListEmptyStateView({
275
307
  if (!emptyState.status) {
276
308
  return (
277
309
  <EmptyState
278
- className="min-h-120 px-16 py-20"
310
+ className="min-h-120 p-panel"
279
311
  icon="componentLine"
280
312
  title={emptyState.title}
281
313
  description={emptyState.description}
@@ -285,7 +317,7 @@ function StepListEmptyStateView({
285
317
  }
286
318
 
287
319
  return (
288
- <div className="flex min-h-120 flex-col items-center justify-center gap-10 px-16 py-20">
320
+ <div className="flex min-h-120 flex-col items-center justify-center gap-inline p-panel">
289
321
  <StepListEmptyStateIcon status={emptyState.status} />
290
322
  <div className="text-center">
291
323
  <Text size="sm" className="text-foreground-neutral-subtle">
@@ -302,14 +334,14 @@ function StepListEmptyStateView({
302
334
  function StepListEmptyStateIcon({status}: {status: JobDisplayStatus}) {
303
335
  if (status !== 'running') {
304
336
  return (
305
- <div className="flex size-32 items-center justify-center rounded-6 border border-border-neutral-strong bg-background-neutral-base p-8">
337
+ <div className="flex size-32 items-center justify-center rounded-6 border border-border-neutral-strong bg-background-neutral-base p-tight">
306
338
  <WorkflowStatusIcon status={status} size={20} tooltip={false} />
307
339
  </div>
308
340
  );
309
341
  }
310
342
 
311
343
  return (
312
- <div className="flex size-32 items-center justify-center rounded-6 border border-border-neutral-strong bg-background-neutral-base p-8 text-foreground-neutral-muted">
344
+ <div className="flex size-32 items-center justify-center rounded-6 border border-border-neutral-strong bg-background-neutral-base p-tight text-foreground-neutral-muted">
313
345
  <Icon name="timerLine" size={18} aria-hidden="true" />
314
346
  </div>
315
347
  );
@@ -319,13 +351,17 @@ function StepRow({
319
351
  entry,
320
352
  selected,
321
353
  hasExpandedContent,
354
+ isLast,
322
355
  onSelect,
356
+ onInspect,
323
357
  expandedContent,
324
358
  }: {
325
359
  entry: StepListEntryModel;
326
360
  selected: boolean;
327
361
  hasExpandedContent: boolean;
362
+ isLast: boolean;
328
363
  onSelect: () => void;
364
+ onInspect?: (() => void) | undefined;
329
365
  expandedContent: ReactNode;
330
366
  }) {
331
367
  const shouldShowLabelTooltip = entry.step.label.length > 32;
@@ -341,7 +377,7 @@ function StepRow({
341
377
  />
342
378
  <StepStatusIcon entry={entry} />
343
379
  <div className="min-w-0 flex-1">
344
- <div className="flex min-w-0 items-center gap-8">
380
+ <div className="flex min-w-0 items-center gap-inline">
345
381
  <Text size="sm" bold className="truncate text-foreground-neutral-base">
346
382
  {entry.step.label}
347
383
  </Text>
@@ -353,8 +389,7 @@ function StepRow({
353
389
  </>
354
390
  );
355
391
  const rowClasses = cn(
356
- 'group grid min-h-44 w-full grid-cols-[14px_14px_minmax(0,1fr)_auto] items-center gap-x-8 px-12 py-6 text-left transition-colors hover:bg-background-components-hover focus-visible:shadow-border-interactive-with-active focus-visible:outline-none',
357
- selected && 'bg-background-components-hover',
392
+ 'group flex min-h-44 min-w-0 flex-1 items-center gap-x-inline bg-transparent px-row py-row text-left transition-colors hover:bg-transparent active:bg-transparent focus-visible:shadow-border-interactive-with-active focus-visible:outline-none',
358
393
  entry.carriedOver && 'opacity-[0.55]',
359
394
  );
360
395
  const button = hasExpandedContent ? (
@@ -388,15 +423,33 @@ function StepRow({
388
423
  );
389
424
  const row = (
390
425
  <>
391
- {triggerNode}
426
+ <div
427
+ className={cn(
428
+ 'group flex min-w-0 items-center gap-inline pr-[8px] transition-colors hover:bg-background-components-hover active:bg-background-components-pressed',
429
+ selected && 'bg-background-components-hover',
430
+ !hasExpandedContent && ['border-b border-border-neutral-base', isLast && 'border-b-0'],
431
+ )}
432
+ >
433
+ <div className="min-w-0 flex-1">{triggerNode}</div>
434
+ {onInspect ? (
435
+ <Tooltip>
436
+ <TooltipTrigger asChild>
437
+ <button
438
+ type="button"
439
+ aria-label={`Inspect ${entry.step.label}, attempt ${entry.attempt}`}
440
+ onClick={onInspect}
441
+ className="flex size-28 shrink-0 items-center justify-center rounded-4 bg-transparent text-foreground-neutral-muted outline-none transition-colors hover:bg-transparent hover:text-foreground-neutral-base active:bg-transparent focus-visible:shadow-button-neutral-focus"
442
+ >
443
+ <Icon name="informationLine" size={14} aria-hidden="true" />
444
+ </button>
445
+ </TooltipTrigger>
446
+ <TooltipContent>Inspect step details</TooltipContent>
447
+ </Tooltip>
448
+ ) : null}
449
+ </div>
392
450
  {selected && expandedContent ? (
393
- <AccordionContent className="border-t border-border-neutral-base bg-background-neutral-base px-12 py-12">
394
- <div className="grid grid-cols-[14px_14px_minmax(0,1fr)_auto] gap-x-8">
395
- <span aria-hidden="true" />
396
- <span aria-hidden="true" />
397
- <div className="min-w-0">{expandedContent}</div>
398
- <span aria-hidden="true" />
399
- </div>
451
+ <AccordionContent className="border-t border-border-neutral-base bg-transparent px-0 py-0">
452
+ <div className="min-w-0">{expandedContent}</div>
400
453
  </AccordionContent>
401
454
  ) : null}
402
455
  </>
@@ -410,7 +463,7 @@ function StepRow({
410
463
  );
411
464
  }
412
465
 
413
- return <li className="border-b border-border-neutral-base last:border-b-0">{row}</li>;
466
+ return <li>{row}</li>;
414
467
  }
415
468
 
416
469
  function CarriedOverBadge() {
@@ -482,10 +535,10 @@ const attemptChipClasses: Record<NonNullable<BadgeVariant>, string> = {
482
535
 
483
536
  function StepAttemptChip({attempt}: {attempt: StepAttemptModel}) {
484
537
  return (
485
- <div className="flex shrink-0 items-center gap-4" aria-hidden="true">
538
+ <div className="flex shrink-0 items-center gap-tight" aria-hidden="true">
486
539
  <span
487
540
  className={cn(
488
- 'inline-flex h-18 min-w-24 items-center justify-center rounded-4 border px-6 font-code text-xs leading-16 text-foreground-neutral-base',
541
+ 'inline-flex h-18 min-w-24 items-center justify-center rounded-4 border px-tight font-code text-xs leading-16 text-foreground-neutral-base',
489
542
  attemptChipClasses[attempt.statusVisual.badge ?? 'neutral'],
490
543
  )}
491
544
  >
@@ -121,7 +121,7 @@ function DurationText({
121
121
  variant="label"
122
122
  aria-label={ariaLabel}
123
123
  className={cn(
124
- 'inline-flex shrink-0 items-center gap-4 tabular-nums text-foreground-neutral-subtle',
124
+ 'inline-flex shrink-0 items-center gap-tight tabular-nums text-foreground-neutral-subtle',
125
125
  className,
126
126
  )}
127
127
  >
@@ -74,9 +74,9 @@ export function JobStatusStrip({jobs, className}: JobStatusStripProps) {
74
74
  <span
75
75
  role="img"
76
76
  aria-label={summary}
77
- className={cn('inline-flex items-center gap-3', className)}
77
+ className={cn('inline-flex items-center gap-[3px]', className)}
78
78
  >
79
- <span aria-hidden="true" className="inline-flex items-center gap-3">
79
+ <span aria-hidden="true" className="inline-flex items-center gap-[3px]">
80
80
  {visible.map((job) => (
81
81
  <WorkflowStatusIcon
82
82
  key={job.id}
@@ -89,7 +89,7 @@ export function JobStatusStrip({jobs, className}: JobStatusStripProps) {
89
89
  />
90
90
  ))}
91
91
  {hiddenCount > 0 && overflowStatus ? (
92
- <span className="inline-flex items-center gap-2">
92
+ <span className="inline-flex items-center gap-[2px]">
93
93
  <WorkflowStatusIcon
94
94
  status={overflowStatus}
95
95
  size={GLYPH_SIZE}
@@ -127,17 +127,17 @@ function JobStatusStripTooltip({jobs, summary}: {jobs: WorkflowRunJobs; summary:
127
127
  const remaining = notableTotal - named.length;
128
128
 
129
129
  return (
130
- <span className="block max-w-[280px]">
130
+ <span className="flex max-w-[280px] flex-col gap-tight">
131
131
  <Text as="span" size="xs" className="block">
132
132
  {summary}
133
133
  </Text>
134
134
  {named.map((job) => (
135
- <Code as="span" variant="label" key={job.id} className="mt-2 block truncate">
135
+ <Code as="span" variant="label" key={job.id} className="block truncate">
136
136
  {getWorkflowStatusVisual(job.status).label.toLowerCase()} · {job.name ?? job.key}
137
137
  </Code>
138
138
  ))}
139
139
  {remaining > 0 ? (
140
- <Text as="span" size="xs" className="mt-2 block text-foreground-neutral-muted">
140
+ <Text as="span" size="xs" className="block text-foreground-neutral-muted">
141
141
  and {remaining} more
142
142
  </Text>
143
143
  ) : null}
@@ -65,7 +65,7 @@ export function WorkflowRunFilterMenu({
65
65
  </DropdownMenuTrigger>
66
66
  <DropdownMenuContent align="start" className="max-h-[320px] w-[240px] overflow-y-auto">
67
67
  {options.length === 0 ? (
68
- <Text as="p" size="xs" className="px-8 py-6 text-foreground-neutral-muted">
68
+ <Text as="p" size="xs" className="px-tight py-[6px] text-foreground-neutral-muted">
69
69
  {emptyMessage}
70
70
  </Text>
71
71
  ) : (
@@ -53,7 +53,7 @@ export function WorkflowRunFilters({
53
53
  const [sheetOpen, setSheetOpen] = useState(false);
54
54
 
55
55
  return (
56
- <div className="flex flex-wrap items-center gap-8">
56
+ <div className="flex flex-wrap items-center gap-inline">
57
57
  <Input
58
58
  value={search.search ?? ''}
59
59
  onChange={(event) => onChange({search: event.target.value})}
@@ -64,7 +64,7 @@ export function WorkflowRunFilters({
64
64
  iconLeft={<Icon name="searchLine" className="size-14 text-foreground-neutral-muted" />}
65
65
  />
66
66
 
67
- <div className="hidden flex-wrap items-center gap-8 md:flex">
67
+ <div className="hidden flex-wrap items-center gap-inline md:flex">
68
68
  <WorkflowRunFilterControls search={search} facets={facets} onChange={onChange} />
69
69
  {hasActiveFilters ? <ClearFiltersButton onClear={onClear} /> : null}
70
70
  </div>
@@ -85,7 +85,7 @@ export function WorkflowRunFilters({
85
85
  <SheetHeader>
86
86
  <SheetTitle>Filter runs</SheetTitle>
87
87
  </SheetHeader>
88
- <SheetBody className="flex flex-col gap-12">
88
+ <SheetBody className="flex flex-col gap-cluster">
89
89
  <WorkflowRunFilterControls
90
90
  search={search}
91
91
  facets={facets}
@@ -161,7 +161,7 @@ function WorkflowRunFilterControls({
161
161
  className={controlClassName}
162
162
  />
163
163
  {stacked ? (
164
- <div className="flex flex-col gap-6">
164
+ <div className="flex flex-col gap-inline">
165
165
  <Label>Created between</Label>
166
166
  <RunDateRangeFilter search={search} onChange={onChange} />
167
167
  </div>
@@ -24,7 +24,7 @@ export function WorkflowRunListSkeleton() {
24
24
  <div
25
25
  // biome-ignore lint/suspicious/noArrayIndexKey: skeleton row, stable position
26
26
  key={index}
27
- className="flex items-center gap-10 px-12 py-8 @min-[976px]:h-44 @min-[976px]:py-0"
27
+ className="flex items-center gap-inline px-row py-row @min-[976px]:h-44 @min-[976px]:py-0"
28
28
  >
29
29
  <Skeleton className="size-14 shrink-0 rounded-full" />
30
30
  <Skeleton className="h-12 w-[220px] max-w-[40%]" />
@@ -44,9 +44,9 @@ export function WorkflowRunListSkeleton() {
44
44
  */
45
45
  export function WorkflowRunListStaleError({query}: {query: QueryLoadErrorQuery}) {
46
46
  return (
47
- <div className="pb-8">
47
+ <div className="pb-[8px]">
48
48
  <Callout role="alert" type="error">
49
- <div className="flex items-center justify-between gap-8">
49
+ <div className="flex items-center justify-between gap-inline">
50
50
  <Text size="xs">Could not refresh workflow runs.</Text>
51
51
  <Button
52
52
  type="button"
@@ -116,7 +116,7 @@ export function WorkflowRunListNoMatches({
116
116
  onLoadMore?: () => void;
117
117
  }) {
118
118
  return (
119
- <div className="flex flex-col gap-8">
119
+ <div className="flex flex-col gap-inline">
120
120
  {isFetchNextPageError ? (
121
121
  <Callout role="alert" type="error">
122
122
  Could not load more workflow runs. Try again to continue searching older runs.
@@ -131,7 +131,7 @@ export function WorkflowRunListNoMatches({
131
131
  : 'No run matches these filters.'
132
132
  }
133
133
  action={
134
- <div className="flex flex-wrap justify-center gap-8">
134
+ <div className="flex flex-wrap justify-center gap-inline">
135
135
  {hasNextPage && onLoadMore ? (
136
136
  <Button
137
137
  type="button"
@@ -167,7 +167,7 @@ export function WorkflowRunListLoadMore({
167
167
  if (!hasNextPage || !onLoadMore) return null;
168
168
 
169
169
  return (
170
- <div className="flex flex-col items-center gap-8 border-t border-border-neutral-base py-12">
170
+ <div className="flex flex-col items-center gap-inline border-t border-border-neutral-base py-row">
171
171
  {isFetchNextPageError ? (
172
172
  <Callout role="alert" type="error">
173
173
  Could not load more workflow runs. Try again.
@@ -58,7 +58,7 @@ export function WorkflowRunListView({
58
58
  <TimeTickerProvider intervalMs={1000} reducedMotionIntervalMs={10_000}>
59
59
  <section
60
60
  aria-labelledby={headingId}
61
- className={cn('flex min-h-0 min-w-0 flex-1 flex-col gap-12', className)}
61
+ className={cn('flex min-h-0 min-w-0 flex-1 flex-col gap-cluster', className)}
62
62
  >
63
63
  {/* The tab strip above already reads "Runs", so the page heading is for structure and
64
64
  assistive tech rather than a second visible title competing with it. */}
@@ -81,15 +81,15 @@ export function WorkflowRunRow({
81
81
  <>
82
82
  <WorkflowStatusIcon status={display.status} size={14} className="shrink-0" />
83
83
 
84
- <div className="flex min-w-0 flex-1 flex-col gap-2 @min-[976px]:flex-row @min-[976px]:items-center @min-[976px]:gap-12">
85
- <span className="flex min-w-0 items-center gap-8 @min-[976px]:flex-1">
84
+ <div className="flex min-w-0 flex-1 flex-col gap-tight @min-[976px]:flex-row @min-[976px]:items-center @min-[976px]:gap-cluster">
85
+ <span className="flex min-w-0 items-center gap-inline @min-[976px]:flex-1">
86
86
  <Code variant="label" bold className="truncate text-foreground-neutral-base">
87
87
  {run.name}
88
88
  </Code>
89
89
  {run.number !== null ? <WorkflowRunNumberLabel run={run} /> : null}
90
90
  </span>
91
91
 
92
- <span className="flex min-w-0 flex-wrap items-center gap-10 text-foreground-neutral-subtle @min-[976px]:flex-nowrap @min-[976px]:shrink-0">
92
+ <span className="flex min-w-0 flex-wrap items-center gap-inline text-foreground-neutral-subtle @min-[976px]:flex-nowrap @min-[976px]:shrink-0">
93
93
  {run.triggerDisplayLabel ? <TriggerLabel run={run} /> : null}
94
94
  {branch ? <BranchLabel branch={branch} isPullRequest={branch.startsWith('#')} /> : null}
95
95
  {commit ? <CommitLabel commit={commit} /> : null}
@@ -97,7 +97,7 @@ export function WorkflowRunRow({
97
97
  </span>
98
98
  </div>
99
99
 
100
- <span className="flex shrink-0 items-center gap-12">
100
+ <span className="flex shrink-0 items-center gap-cluster">
101
101
  {/* Fixed at the strip's worst case (see MAX_VISIBLE_JOBS) so the overflow count cannot
102
102
  paint over the duration, and kept even when a run has no jobs planned yet, so the
103
103
  numerics stay in line down the list instead of stepping in and out. */}
@@ -120,7 +120,7 @@ export function WorkflowRunRow({
120
120
  // Rows run edge to edge inside the list's scroll container, which would clip the standard
121
121
  // outset focus ring, so this one is inset per the design system's focus-ring rule.
122
122
  const rowClassName =
123
- 'flex w-full min-w-0 items-center gap-10 px-12 py-8 text-left transition-colors hover:bg-background-components-hover focus-visible:shadow-[inset_0_0_0_2px_var(--color-primary-500)] focus-visible:outline-none @min-[976px]:h-44 @min-[976px]:py-0';
123
+ 'flex w-full min-w-0 items-center gap-inline px-row py-row text-left transition-colors hover:bg-background-components-hover focus-visible:shadow-[inset_0_0_0_2px_var(--color-primary-500)] focus-visible:outline-none @min-[976px]:h-44 @min-[976px]:py-0';
124
124
 
125
125
  // Optimistic manual runs (temp-<uuid>) have no detail page until the canonical row
126
126
  // replaces them on the next poll, so they render non-interactively instead of as a link
@@ -164,7 +164,7 @@ function TriggerLabel({run}: {run: WorkflowRunListItem}) {
164
164
  return (
165
165
  <Tooltip>
166
166
  <TooltipTrigger asChild>
167
- <span className="flex min-w-0 max-w-[140px] items-center gap-6 outline-none">
167
+ <span className="flex min-w-0 max-w-[140px] items-center gap-tight outline-none">
168
168
  <TriggerSourceIcon
169
169
  provider={run.triggerProvider}
170
170
  source={run.triggerSource}
@@ -235,7 +235,7 @@ function MetadataChip({
235
235
  children: string;
236
236
  }) {
237
237
  return (
238
- <span className={cn('flex min-w-0 shrink-0 items-center gap-4', className)} title={title}>
238
+ <span className={cn('flex min-w-0 shrink-0 items-center gap-tight', className)} title={title}>
239
239
  <Icon name={icon} className="size-12 shrink-0 text-foreground-neutral-muted" aria-hidden />
240
240
  <Code as="span" variant="label" className="min-w-0 truncate">
241
241
  {children}
@@ -52,7 +52,7 @@ export function WorkflowRunAttemptSwitcher({
52
52
  size="2xs"
53
53
  iconRight="arrowDownSLine"
54
54
  aria-label={`Switch attempt, currently ${run.runAttempt.attempt} of ${maxAttempt}`}
55
- className="h-20 px-4 text-foreground-neutral-subtle hover:text-foreground-neutral-base"
55
+ className="h-20 px-[4px] text-foreground-neutral-subtle hover:text-foreground-neutral-base"
56
56
  >
57
57
  <Text as="span" size="xs" className="text-inherit">
58
58
  Attempt {run.runAttempt.attempt} of {maxAttempt}
@@ -38,7 +38,7 @@ describe('WorkflowRunSummary', () => {
38
38
 
39
39
  const summary = await screen.findByRole('region', {name: 'deploy-web'});
40
40
 
41
- expect(summary).toHaveClass('bg-background-neutral-background', 'px-16');
41
+ expect(summary).toHaveClass('bg-background-neutral-background', 'px-row', 'py-row');
42
42
  expect(summary.firstElementChild).not.toHaveClass('max-w-[1120px]');
43
43
  expect(within(summary).getByRole('heading', {name: 'deploy-web'})).toBeInTheDocument();
44
44
  expect(within(summary).getAllByText('Running')).not.toHaveLength(0);
@@ -1,4 +1,5 @@
1
1
  import {TriggerSourceIcon} from '@shipfox/client-triggers';
2
+ import {MetadataSeparator} from '@shipfox/client-ui';
2
3
  import {Badge} from '@shipfox/react-ui/badge';
3
4
  import {Button} from '@shipfox/react-ui/button';
4
5
  import {
@@ -77,10 +78,13 @@ export function WorkflowRunSummary({
77
78
 
78
79
  return (
79
80
  <TimeTickerProvider intervalMs={1000} reducedMotionIntervalMs={10_000}>
80
- <section aria-labelledby={headingId} className="bg-background-neutral-background px-16 py-12">
81
- <div className="grid min-w-0 grid-cols-[minmax(0,1fr)_auto] items-center gap-x-12 gap-y-8 overflow-hidden max-[480px]:grid-cols-1">
81
+ <section
82
+ aria-labelledby={headingId}
83
+ className="bg-background-neutral-background px-row py-row"
84
+ >
85
+ <div className="grid min-w-0 grid-cols-[minmax(0,1fr)_auto] items-center gap-x-cluster gap-y-inline overflow-hidden max-[480px]:grid-cols-1">
82
86
  <div className="col-start-1 row-start-1 min-w-0 max-[480px]:col-start-auto max-[480px]:row-start-auto">
83
- <div className="flex min-w-0 items-center gap-8">
87
+ <div className="flex min-w-0 items-center gap-inline">
84
88
  <Badge variant={status.badge} size="xs">
85
89
  <span className="text-center" style={{width: `${STATUS_BADGE_LABEL_WIDTH_CH}ch`}}>
86
90
  {status.label}
@@ -107,7 +111,7 @@ export function WorkflowRunSummary({
107
111
  </div>
108
112
 
109
113
  {hasAction ? (
110
- <div className="col-start-2 row-start-1 flex min-w-max items-center gap-6 justify-self-end max-[480px]:col-start-auto max-[480px]:row-start-auto max-[480px]:justify-self-start">
114
+ <div className="col-start-2 row-start-1 flex min-w-max items-center gap-inline justify-self-end max-[480px]:col-start-auto max-[480px]:row-start-auto max-[480px]:justify-self-start">
111
115
  <WorkflowRunActionSlot
112
116
  action={action}
113
117
  cancelling={cancelling}
@@ -118,7 +122,7 @@ export function WorkflowRunSummary({
118
122
  </div>
119
123
  ) : null}
120
124
 
121
- <div className="col-span-2 row-start-2 flex min-w-0 items-center gap-12 overflow-hidden text-foreground-neutral-subtle max-[480px]:col-span-1 max-[480px]:row-start-auto">
125
+ <div className="col-span-2 row-start-2 flex min-w-0 items-center gap-cluster overflow-hidden text-foreground-neutral-subtle max-[480px]:col-span-1 max-[480px]:row-start-auto">
122
126
  {run.number !== null ? (
123
127
  <>
124
128
  <WorkflowRunNumberLabel run={run} />
@@ -144,7 +148,7 @@ export function WorkflowRunSummary({
144
148
  <button
145
149
  type="button"
146
150
  aria-label={run.triggerLabel}
147
- className="inline-flex max-w-full min-w-0 cursor-help items-center gap-4 rounded-6 border-0 bg-transparent p-0 text-left text-foreground-neutral-subtle outline-none focus-visible:shadow-button-neutral-focus"
151
+ className="inline-flex max-w-full min-w-0 cursor-help items-center gap-tight rounded-6 border-0 bg-transparent p-0 text-left text-foreground-neutral-subtle outline-none focus-visible:shadow-button-neutral-focus"
148
152
  >
149
153
  <TriggerSourceIcon
150
154
  provider={run.triggerProvider}
@@ -188,7 +192,7 @@ export function WorkflowRunSummary({
188
192
  {blockingJob ? (
189
193
  <>
190
194
  <MetadataSeparator />
191
- <span className="flex min-w-0 items-center gap-4">
195
+ <span className="flex min-w-0 items-center gap-tight">
192
196
  <Text as="span" size="xs" className="shrink-0">
193
197
  waiting on
194
198
  </Text>
@@ -310,7 +314,3 @@ function hasFailedOrCancelledJobs(run: WorkflowRunDetail): boolean {
310
314
  function workflowRunHasJobs(run: WorkflowRunDetail): run is WorkflowRunDetail & {jobs: Job[]} {
311
315
  return 'jobs' in run && Array.isArray(run.jobs);
312
316
  }
313
-
314
- export function MetadataSeparator() {
315
- return <span aria-hidden="true" className="h-12 w-px shrink-0 bg-border-neutral-base" />;
316
- }