Skip to content

Commit ba719ff

Browse files
authored
Merge pull request #2701 from trycompai/fix/pentest-loading-shift
fix(pentest): loading shell + mobile-friendly split-view
2 parents 6857758 + aea5f1a commit ba719ff

12 files changed

Lines changed: 423 additions & 20 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import {
2+
DetailMainSkeleton,
3+
LoadingShell,
4+
} from '../_components/LoadingShell';
5+
6+
/** Detail-route loading state. Mobile shows ONLY the detail-shape main pane. */
7+
export default function Loading() {
8+
return <LoadingShell variant="detail" mainPane={<DetailMainSkeleton />} />;
9+
}

apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/CompletedDetail.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export function CompletedDetail({
4343

4444
return (
4545
<div className="min-h-0 overflow-y-auto">
46-
<div className="mx-auto max-w-5xl px-8 py-8 space-y-6">
46+
<div className="mx-auto max-w-5xl space-y-6 px-4 py-6 md:px-8 md:py-8">
4747
<header className="space-y-3 pb-3">
4848
<div className="flex items-center gap-3">
4949
<StatusPill status="completed" findingCount={issues.length} />

apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/EmptyState.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export function EmptyState({
4646
? "You've used your trial run. Paid plans are coming soon — contact support if you need access today."
4747
: 'Automated black-box pen testing. Start a scan to see findings here.';
4848
return (
49-
<div className="mx-auto flex h-full max-w-3xl flex-col items-start justify-center gap-6 px-8 py-12">
49+
<div className="mx-auto flex h-full max-w-3xl flex-col items-start justify-center gap-6 px-4 py-10 md:px-8 md:py-12">
5050
<div>
5151
<div className="mb-3 inline-flex items-center gap-2">
5252
<h1 className="text-[26px] font-medium tracking-[-0.02em]">

apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/FailedDetail.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export function FailedDetail({ run, onRetry }: FailedDetailProps) {
1616

1717
return (
1818
<div className="min-h-0 overflow-y-auto">
19-
<div className="mx-auto max-w-5xl px-8 py-8 space-y-6">
19+
<div className="mx-auto max-w-5xl space-y-6 px-4 py-6 md:px-8 md:py-8">
2020
<header className="space-y-3">
2121
<div className="flex items-center gap-3">
2222
<StatusPill status={run.status} />

apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/FindingDetail.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export function FindingDetail({ issue, onBack }: FindingDetailProps) {
3434

3535
return (
3636
<div className="min-h-0 overflow-y-auto">
37-
<div className="mx-auto max-w-5xl px-8 py-8 space-y-6">
37+
<div className="mx-auto max-w-5xl space-y-6 px-4 py-6 md:px-8 md:py-8">
3838
<div>
3939
<Button variant="ghost" size="sm" onClick={onBack}>
4040
<ArrowLeft className="h-3.5 w-3.5" />
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
/**
2+
* Shared loading skeleton for pentest routes. Mirrors the SplitView shell
3+
* one-to-one (full-bleed, 340px sidebar + main pane on desktop) so a hard
4+
* refresh transitions into the live page without a CLS jump.
5+
*
6+
* On mobile (<md) only ONE pane is rendered, picked by `variant` to match
7+
* the route the user is hard-refreshing on:
8+
* - 'list' — only the sidebar shape
9+
* - 'detail' — only the main pane shape
10+
* - 'create' — only the main pane shape
11+
*
12+
* Desktop always shows both. Pure JSX, no hooks — safe to consume from
13+
* server-component `loading.tsx` files.
14+
*/
15+
type Variant = 'list' | 'detail' | 'create';
16+
17+
interface LoadingShellProps {
18+
variant: Variant;
19+
/** Right-pane skeleton — typed/lined to roughly match the resolving page. */
20+
mainPane: React.ReactNode;
21+
}
22+
23+
export function LoadingShell({ variant, mainPane }: LoadingShellProps) {
24+
const showSidebarMobile = variant === 'list';
25+
const showMainMobile = !showSidebarMobile;
26+
27+
return (
28+
<div className="flex h-[calc(100vh-4rem)] min-h-0 -m-4 md:-m-6">
29+
<aside
30+
className={[
31+
'flex h-full min-h-0 w-full md:w-[340px] md:shrink-0 flex-col border-r border-border bg-background',
32+
showSidebarMobile ? 'flex' : 'hidden md:flex',
33+
].join(' ')}
34+
aria-hidden
35+
>
36+
<div className="flex items-center justify-between border-b border-border px-4 py-3">
37+
<div className="flex items-center gap-2">
38+
<div className="h-4 w-12 rounded bg-muted animate-pulse" />
39+
<div className="h-4 w-6 rounded bg-muted animate-pulse" />
40+
</div>
41+
<div className="h-6 w-14 rounded border border-border bg-muted/50 animate-pulse" />
42+
</div>
43+
<ul className="flex-1 min-h-0 divide-y divide-border overflow-hidden">
44+
{Array.from({ length: 5 }).map((_, i) => (
45+
<li key={i} className="flex flex-col gap-1.5 px-4 py-3">
46+
<div className="flex items-center gap-2">
47+
<div className="h-4 w-20 rounded-full bg-muted animate-pulse" />
48+
<div className="h-3 w-14 rounded bg-muted animate-pulse" />
49+
</div>
50+
<div className="h-4 w-3/4 rounded bg-muted animate-pulse" />
51+
<div className="h-3 w-1/3 rounded bg-muted animate-pulse" />
52+
</li>
53+
))}
54+
</ul>
55+
<div className="border-t border-border px-4 py-3">
56+
<div className="h-3 w-2/3 rounded bg-muted animate-pulse" />
57+
</div>
58+
</aside>
59+
<main
60+
className={[
61+
'min-w-0 flex-1 flex-col',
62+
showMainMobile ? 'flex' : 'hidden md:flex',
63+
].join(' ')}
64+
>
65+
{/* Mobile-only back-bar skeleton on non-list routes. Matches the
66+
"← Scans" bar that SplitView renders on detail/create URLs
67+
below md — without this placeholder, resolving the page
68+
shifts the main content down ~33px when the real bar mounts. */}
69+
{variant !== 'list' && (
70+
<div
71+
className="md:hidden flex items-center border-b border-border bg-background px-3 py-2"
72+
aria-hidden
73+
>
74+
<div className="h-4 w-16 rounded bg-muted animate-pulse" />
75+
</div>
76+
)}
77+
{mainPane}
78+
</main>
79+
</div>
80+
);
81+
}
82+
83+
/** Overview-shape main-pane skeleton (header + hero card + 4-stat band). */
84+
export function OverviewMainSkeleton() {
85+
return (
86+
<div className="min-h-0 flex-1 overflow-hidden">
87+
<div className="mx-auto max-w-5xl space-y-6 px-4 py-6 md:px-8 md:py-8">
88+
<div className="flex items-end justify-between gap-3 pb-3">
89+
<div className="space-y-2">
90+
<div className="h-7 w-40 rounded bg-muted animate-pulse" />
91+
<div className="h-3 w-72 rounded bg-muted animate-pulse" />
92+
</div>
93+
<div className="h-9 w-28 rounded border border-border bg-muted/50 animate-pulse" />
94+
</div>
95+
<div className="rounded border border-border p-6 space-y-3">
96+
<div className="h-4 w-32 rounded bg-muted animate-pulse" />
97+
<div className="h-9 w-1/2 rounded bg-muted animate-pulse" />
98+
<div className="h-3 w-2/3 rounded bg-muted animate-pulse" />
99+
</div>
100+
<div className="grid grid-cols-2 gap-6 border-b-2 border-border pb-6 md:grid-cols-4">
101+
{Array.from({ length: 4 }).map((_, i) => (
102+
<div
103+
key={i}
104+
className={
105+
i > 0
106+
? 'space-y-3 md:border-l md:border-border md:pl-6'
107+
: 'space-y-3'
108+
}
109+
>
110+
<div className="h-3 w-20 rounded bg-muted animate-pulse" />
111+
<div className="h-10 w-16 rounded bg-muted animate-pulse" />
112+
<div className="h-3 w-24 rounded bg-muted animate-pulse" />
113+
</div>
114+
))}
115+
</div>
116+
</div>
117+
</div>
118+
);
119+
}
120+
121+
/** Detail-shape main-pane skeleton (header + sev tally + agent grid + table). */
122+
export function DetailMainSkeleton() {
123+
return (
124+
<div className="min-h-0 flex-1 overflow-hidden">
125+
<div className="mx-auto max-w-5xl space-y-6 px-4 py-6 md:px-8 md:py-8">
126+
<div className="space-y-3">
127+
<div className="flex items-center gap-3">
128+
<div className="h-5 w-20 rounded-full bg-muted animate-pulse" />
129+
<div className="h-3 w-32 rounded bg-muted animate-pulse" />
130+
</div>
131+
<div className="h-7 w-2/3 rounded bg-muted animate-pulse" />
132+
<div className="flex flex-wrap gap-x-6 gap-y-1">
133+
<div className="h-3 w-40 rounded bg-muted animate-pulse" />
134+
<div className="h-3 w-44 rounded bg-muted animate-pulse" />
135+
</div>
136+
</div>
137+
<div className="space-y-2">
138+
<div className="h-3 w-24 rounded bg-muted animate-pulse" />
139+
<div className="grid grid-cols-11 gap-1.5">
140+
{Array.from({ length: 22 }).map((_, i) => (
141+
<div
142+
key={i}
143+
className="aspect-square rounded bg-muted animate-pulse"
144+
/>
145+
))}
146+
</div>
147+
<div className="h-3 w-32 rounded bg-muted animate-pulse" />
148+
</div>
149+
<div className="space-y-2">
150+
<div className="h-3 w-28 rounded bg-muted animate-pulse" />
151+
<div className="grid grid-cols-2 gap-3 sm:grid-cols-5">
152+
{Array.from({ length: 5 }).map((_, i) => (
153+
<div
154+
key={i}
155+
className="h-12 rounded border border-border bg-muted/40 animate-pulse"
156+
/>
157+
))}
158+
</div>
159+
</div>
160+
<div className="space-y-2">
161+
<div className="h-3 w-24 rounded bg-muted animate-pulse" />
162+
<div className="space-y-1 rounded border border-border p-2">
163+
{Array.from({ length: 4 }).map((_, i) => (
164+
<div
165+
key={i}
166+
className="h-8 rounded bg-muted animate-pulse"
167+
/>
168+
))}
169+
</div>
170+
</div>
171+
</div>
172+
</div>
173+
);
174+
}
175+
176+
/** Create-form-shape main-pane skeleton. */
177+
export function CreateMainSkeleton() {
178+
return (
179+
<div className="min-h-0 flex-1 overflow-hidden">
180+
<div className="mx-auto w-full max-w-[680px] px-4 py-8 md:px-6 md:py-10">
181+
<div className="rounded-[var(--radius)] border border-border bg-card p-6 md:p-8 space-y-5">
182+
<div className="space-y-2">
183+
<div className="h-3 w-20 rounded bg-muted animate-pulse" />
184+
<div className="h-6 w-2/3 rounded bg-muted animate-pulse" />
185+
<div className="h-3 w-full rounded bg-muted animate-pulse" />
186+
</div>
187+
{Array.from({ length: 2 }).map((_, i) => (
188+
<div key={i} className="space-y-1.5">
189+
<div className="h-3 w-24 rounded bg-muted animate-pulse" />
190+
<div className="h-9 rounded border border-border bg-muted/40 animate-pulse" />
191+
<div className="h-3 w-3/4 rounded bg-muted animate-pulse" />
192+
</div>
193+
))}
194+
<div className="rounded border border-border bg-muted/40 p-3.5 space-y-2">
195+
<div className="h-3 w-24 rounded bg-muted animate-pulse" />
196+
{Array.from({ length: 3 }).map((_, i) => (
197+
<div key={i} className="h-3 w-full rounded bg-muted animate-pulse" />
198+
))}
199+
</div>
200+
<div className="flex justify-end gap-2">
201+
<div className="h-9 w-20 rounded border border-border bg-muted/40 animate-pulse" />
202+
<div className="h-9 w-28 rounded bg-muted animate-pulse" />
203+
</div>
204+
</div>
205+
</div>
206+
</div>
207+
);
208+
}

0 commit comments

Comments
 (0)