Improve workspace detail mobile layouts

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta 2026-03-28 19:28:11 -05:00
parent caa7550e9f
commit 0356040a29
2 changed files with 414 additions and 403 deletions

View file

@ -161,9 +161,9 @@ function Field({
}) { }) {
return ( return (
<label className="space-y-1.5"> <label className="space-y-1.5">
<div className="flex items-center justify-between gap-3"> <div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between sm:gap-3">
<span className="text-xs font-medium uppercase tracking-[0.16em] text-muted-foreground">{label}</span> <span className="text-xs font-medium uppercase tracking-[0.16em] text-muted-foreground">{label}</span>
{hint ? <span className="text-[11px] text-muted-foreground">{hint}</span> : null} {hint ? <span className="text-[11px] leading-relaxed text-muted-foreground sm:text-right">{hint}</span> : null}
</div> </div>
{children} {children}
</label> </label>
@ -172,8 +172,8 @@ function Field({
function DetailRow({ label, children }: { label: string; children: React.ReactNode }) { function DetailRow({ label, children }: { label: string; children: React.ReactNode }) {
return ( return (
<div className="flex items-start gap-3 py-1.5"> <div className="flex flex-col gap-1.5 py-1.5 sm:flex-row sm:items-start sm:gap-3">
<div className="w-32 shrink-0 text-xs text-muted-foreground">{label}</div> <div className="shrink-0 text-xs text-muted-foreground sm:w-32">{label}</div>
<div className="min-w-0 flex-1 text-sm">{children}</div> <div className="min-w-0 flex-1 text-sm">{children}</div>
</div> </div>
); );
@ -189,7 +189,7 @@ function StatusPill({ children, className }: { children: React.ReactNode; classN
function MonoValue({ value, copy }: { value: string; copy?: boolean }) { function MonoValue({ value, copy }: { value: string; copy?: boolean }) {
return ( return (
<div className="inline-flex max-w-full items-center gap-2"> <div className="inline-flex max-w-full items-start gap-2">
<span className="break-all font-mono text-xs">{value}</span> <span className="break-all font-mono text-xs">{value}</span>
{copy ? ( {copy ? (
<CopyText text={value} className="shrink-0 text-muted-foreground hover:text-foreground" copiedLabel="Copied"> <CopyText text={value} className="shrink-0 text-muted-foreground hover:text-foreground" copiedLabel="Copied">
@ -384,7 +384,7 @@ export function ExecutionWorkspaceDetail() {
<div className="grid gap-6 lg:grid-cols-[minmax(0,1.4fr)_minmax(18rem,0.95fr)]"> <div className="grid gap-6 lg:grid-cols-[minmax(0,1.4fr)_minmax(18rem,0.95fr)]">
<div className="space-y-6"> <div className="space-y-6">
<div className="rounded-2xl border border-border bg-card p-5"> <div className="rounded-2xl border border-border bg-card p-5">
<div className="flex flex-wrap items-start justify-between gap-4"> <div className="flex flex-col gap-4 sm:flex-row sm:flex-wrap sm:items-start sm:justify-between">
<div className="space-y-2"> <div className="space-y-2">
<div className="text-xs font-medium uppercase tracking-[0.16em] text-muted-foreground"> <div className="text-xs font-medium uppercase tracking-[0.16em] text-muted-foreground">
Execution workspace Execution workspace
@ -396,9 +396,10 @@ export function ExecutionWorkspaceDetail() {
and runtime-service behavior in sync with the actual workspace being reused. and runtime-service behavior in sync with the actual workspace being reused.
</p> </p>
</div> </div>
<div className="flex shrink-0 items-center gap-2"> <div className="flex w-full shrink-0 items-center gap-2 sm:w-auto">
<Button <Button
variant="outline" variant="outline"
className="w-full sm:w-auto"
onClick={() => setCloseDialogOpen(true)} onClick={() => setCloseDialogOpen(true)}
disabled={workspace.status === "archived"} disabled={workspace.status === "archived"}
> >
@ -496,7 +497,7 @@ export function ExecutionWorkspaceDetail() {
</Field> </Field>
<div className="rounded-xl border border-dashed border-border/70 bg-muted/20 px-3 py-3"> <div className="rounded-xl border border-dashed border-border/70 bg-muted/20 px-3 py-3">
<div className="flex flex-wrap items-center justify-between gap-3"> <div className="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between">
<div> <div>
<div className="text-xs font-medium uppercase tracking-[0.16em] text-muted-foreground"> <div className="text-xs font-medium uppercase tracking-[0.16em] text-muted-foreground">
Runtime config source Runtime config source
@ -511,6 +512,7 @@ export function ExecutionWorkspaceDetail() {
</div> </div>
<Button <Button
variant="outline" variant="outline"
className="w-full sm:w-auto"
size="sm" size="sm"
disabled={!linkedProjectWorkspace?.runtimeConfig?.workspaceRuntime} disabled={!linkedProjectWorkspace?.runtimeConfig?.workspaceRuntime}
onClick={() => onClick={() =>
@ -548,13 +550,14 @@ export function ExecutionWorkspaceDetail() {
</Field> </Field>
</div> </div>
<div className="mt-5 flex flex-wrap items-center gap-3"> <div className="mt-5 flex flex-col items-stretch gap-3 sm:flex-row sm:flex-wrap sm:items-center">
<Button disabled={!isDirty || updateWorkspace.isPending} onClick={saveChanges}> <Button className="w-full sm:w-auto" disabled={!isDirty || updateWorkspace.isPending} onClick={saveChanges}>
{updateWorkspace.isPending ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null} {updateWorkspace.isPending ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
Save changes Save changes
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
className="w-full sm:w-auto"
disabled={!isDirty || updateWorkspace.isPending} disabled={!isDirty || updateWorkspace.isPending}
onClick={() => { onClick={() => {
setForm(initialState); setForm(initialState);
@ -631,12 +634,12 @@ export function ExecutionWorkspaceDetail() {
</DetailRow> </DetailRow>
<DetailRow label="Repo URL"> <DetailRow label="Repo URL">
{workspace.repoUrl && isSafeExternalUrl(workspace.repoUrl) ? ( {workspace.repoUrl && isSafeExternalUrl(workspace.repoUrl) ? (
<div className="inline-flex items-center gap-2"> <div className="inline-flex max-w-full items-start gap-2">
<a href={workspace.repoUrl} target="_blank" rel="noreferrer" className="inline-flex items-center gap-1 hover:underline"> <a href={workspace.repoUrl} target="_blank" rel="noreferrer" className="inline-flex min-w-0 items-center gap-1 break-all hover:underline">
{workspace.repoUrl} {workspace.repoUrl}
<ExternalLink className="h-3.5 w-3.5" /> <ExternalLink className="h-3.5 w-3.5 shrink-0" />
</a> </a>
<CopyText text={workspace.repoUrl} className="text-muted-foreground hover:text-foreground" copiedLabel="Copied"> <CopyText text={workspace.repoUrl} className="shrink-0 text-muted-foreground hover:text-foreground" copiedLabel="Copied">
<Copy className="h-3.5 w-3.5" /> <Copy className="h-3.5 w-3.5" />
</CopyText> </CopyText>
</div> </div>
@ -662,7 +665,7 @@ export function ExecutionWorkspaceDetail() {
</div> </div>
<div className="rounded-2xl border border-border bg-card p-5"> <div className="rounded-2xl border border-border bg-card p-5">
<div className="flex items-start justify-between gap-3"> <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="space-y-1"> <div className="space-y-1">
<div className="text-xs font-medium uppercase tracking-[0.16em] text-muted-foreground">Runtime services</div> <div className="text-xs font-medium uppercase tracking-[0.16em] text-muted-foreground">Runtime services</div>
<h2 className="text-lg font-semibold">Attached services</h2> <h2 className="text-lg font-semibold">Attached services</h2>
@ -674,10 +677,11 @@ export function ExecutionWorkspaceDetail() {
: "none"} : "none"}
</p> </p>
</div> </div>
<div className="flex shrink-0 flex-wrap gap-2"> <div className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:flex-wrap">
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="w-full sm:w-auto"
disabled={controlRuntimeServices.isPending || !effectiveRuntimeConfig || !workspace.cwd} disabled={controlRuntimeServices.isPending || !effectiveRuntimeConfig || !workspace.cwd}
onClick={() => controlRuntimeServices.mutate("start")} onClick={() => controlRuntimeServices.mutate("start")}
> >
@ -687,6 +691,7 @@ export function ExecutionWorkspaceDetail() {
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="w-full sm:w-auto"
disabled={controlRuntimeServices.isPending || !effectiveRuntimeConfig || !workspace.cwd} disabled={controlRuntimeServices.isPending || !effectiveRuntimeConfig || !workspace.cwd}
onClick={() => controlRuntimeServices.mutate("restart")} onClick={() => controlRuntimeServices.mutate("restart")}
> >
@ -695,6 +700,7 @@ export function ExecutionWorkspaceDetail() {
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="w-full sm:w-auto"
disabled={controlRuntimeServices.isPending || (workspace.runtimeServices?.length ?? 0) === 0} disabled={controlRuntimeServices.isPending || (workspace.runtimeServices?.length ?? 0) === 0}
onClick={() => controlRuntimeServices.mutate("stop")} onClick={() => controlRuntimeServices.mutate("stop")}
> >
@ -707,7 +713,7 @@ export function ExecutionWorkspaceDetail() {
<div className="space-y-3"> <div className="space-y-3">
{workspace.runtimeServices.map((service) => ( {workspace.runtimeServices.map((service) => (
<div key={service.id} className="rounded-xl border border-border/80 bg-background px-3 py-2"> <div key={service.id} className="rounded-xl border border-border/80 bg-background px-3 py-2">
<div className="flex items-start justify-between gap-3"> <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="space-y-1"> <div className="space-y-1">
<div className="text-sm font-medium">{service.serviceName}</div> <div className="text-sm font-medium">{service.serviceName}</div>
<div className="text-xs text-muted-foreground">{service.status} · {service.lifecycle}</div> <div className="text-xs text-muted-foreground">{service.status} · {service.lifecycle}</div>
@ -723,7 +729,7 @@ export function ExecutionWorkspaceDetail() {
{service.cwd ? <MonoValue value={service.cwd} copy /> : null} {service.cwd ? <MonoValue value={service.cwd} copy /> : null}
</div> </div>
</div> </div>
<StatusPill>{service.healthStatus}</StatusPill> <StatusPill className="self-start">{service.healthStatus}</StatusPill>
</div> </div>
</div> </div>
))} ))}
@ -755,7 +761,7 @@ export function ExecutionWorkspaceDetail() {
<div className="space-y-3"> <div className="space-y-3">
{workspaceOperationsQuery.data.slice(0, 6).map((operation) => ( {workspaceOperationsQuery.data.slice(0, 6).map((operation) => (
<div key={operation.id} className="rounded-xl border border-border/80 bg-background px-3 py-2"> <div key={operation.id} className="rounded-xl border border-border/80 bg-background px-3 py-2">
<div className="flex items-start justify-between gap-3"> <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="space-y-1"> <div className="space-y-1">
<div className="text-sm font-medium">{operation.command ?? operation.phase}</div> <div className="text-sm font-medium">{operation.command ?? operation.phase}</div>
<div className="text-xs text-muted-foreground"> <div className="text-xs text-muted-foreground">
@ -768,7 +774,7 @@ export function ExecutionWorkspaceDetail() {
<div className="whitespace-pre-wrap break-words text-xs text-muted-foreground">{operation.stdoutExcerpt}</div> <div className="whitespace-pre-wrap break-words text-xs text-muted-foreground">{operation.stdoutExcerpt}</div>
) : null} ) : null}
</div> </div>
<StatusPill>{operation.status}</StatusPill> <StatusPill className="self-start">{operation.status}</StatusPill>
</div> </div>
</div> </div>
))} ))}

View file

@ -186,9 +186,9 @@ function Field({
}) { }) {
return ( return (
<label className="space-y-1.5"> <label className="space-y-1.5">
<div className="flex items-center justify-between gap-3"> <div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between sm:gap-3">
<span className="text-xs font-medium uppercase tracking-[0.16em] text-muted-foreground">{label}</span> <span className="text-xs font-medium uppercase tracking-[0.16em] text-muted-foreground">{label}</span>
{hint ? <span className="text-[11px] text-muted-foreground">{hint}</span> : null} {hint ? <span className="text-[11px] leading-relaxed text-muted-foreground sm:text-right">{hint}</span> : null}
</div> </div>
{children} {children}
</label> </label>
@ -197,8 +197,8 @@ function Field({
function DetailRow({ label, children }: { label: string; children: React.ReactNode }) { function DetailRow({ label, children }: { label: string; children: React.ReactNode }) {
return ( return (
<div className="flex items-start gap-3 py-1.5"> <div className="flex flex-col gap-1.5 py-1.5 sm:flex-row sm:items-start sm:gap-3">
<div className="w-28 shrink-0 text-xs text-muted-foreground">{label}</div> <div className="shrink-0 text-xs text-muted-foreground sm:w-28">{label}</div>
<div className="min-w-0 flex-1 text-sm">{children}</div> <div className="min-w-0 flex-1 text-sm">{children}</div>
</div> </div>
); );
@ -364,7 +364,7 @@ export function ProjectWorkspaceDetail() {
<div className="grid gap-6 lg:grid-cols-[minmax(0,1.4fr)_minmax(18rem,0.9fr)]"> <div className="grid gap-6 lg:grid-cols-[minmax(0,1.4fr)_minmax(18rem,0.9fr)]">
<div className="space-y-6"> <div className="space-y-6">
<div className="rounded-2xl border border-border bg-card p-5"> <div className="rounded-2xl border border-border bg-card p-5">
<div className="flex flex-wrap items-start justify-between gap-4"> <div className="flex flex-col gap-4 sm:flex-row sm:flex-wrap sm:items-start sm:justify-between">
<div className="space-y-2"> <div className="space-y-2">
<div className="text-xs font-medium uppercase tracking-[0.16em] text-muted-foreground"> <div className="text-xs font-medium uppercase tracking-[0.16em] text-muted-foreground">
Project workspace Project workspace
@ -379,6 +379,7 @@ export function ProjectWorkspaceDetail() {
{!workspace.isPrimary ? ( {!workspace.isPrimary ? (
<Button <Button
variant="outline" variant="outline"
className="w-full sm:w-auto"
disabled={setPrimaryWorkspace.isPending} disabled={setPrimaryWorkspace.isPending}
onClick={() => setPrimaryWorkspace.mutate()} onClick={() => setPrimaryWorkspace.mutate()}
> >
@ -388,7 +389,7 @@ export function ProjectWorkspaceDetail() {
Make primary Make primary
</Button> </Button>
) : ( ) : (
<div className="inline-flex items-center gap-2 rounded-xl border border-emerald-500/25 bg-emerald-500/10 px-3 py-2 text-sm text-emerald-700 dark:text-emerald-300"> <div className="inline-flex items-center gap-2 rounded-xl border border-emerald-500/25 bg-emerald-500/10 px-3 py-2 text-sm text-emerald-700 dark:text-emerald-300 sm:max-w-sm">
<Sparkles className="h-4 w-4" /> <Sparkles className="h-4 w-4" />
This is the projects primary codebase workspace. This is the projects primary codebase workspace.
</div> </div>
@ -537,13 +538,14 @@ export function ProjectWorkspaceDetail() {
</Field> </Field>
</div> </div>
<div className="mt-5 flex flex-wrap items-center gap-3"> <div className="mt-5 flex flex-col items-stretch gap-3 sm:flex-row sm:flex-wrap sm:items-center">
<Button disabled={!isDirty || updateWorkspace.isPending} onClick={saveChanges}> <Button className="w-full sm:w-auto" disabled={!isDirty || updateWorkspace.isPending} onClick={saveChanges}>
{updateWorkspace.isPending ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null} {updateWorkspace.isPending ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
Save changes Save changes
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
className="w-full sm:w-auto"
disabled={!isDirty || updateWorkspace.isPending} disabled={!isDirty || updateWorkspace.isPending}
onClick={() => { onClick={() => {
setForm(initialState); setForm(initialState);
@ -590,7 +592,7 @@ export function ProjectWorkspaceDetail() {
</div> </div>
<div className="rounded-2xl border border-border bg-card p-5"> <div className="rounded-2xl border border-border bg-card p-5">
<div className="flex items-start justify-between gap-3"> <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="space-y-1"> <div className="space-y-1">
<div className="text-xs font-medium uppercase tracking-[0.16em] text-muted-foreground">Runtime services</div> <div className="text-xs font-medium uppercase tracking-[0.16em] text-muted-foreground">Runtime services</div>
<h2 className="text-lg font-semibold">Attached services</h2> <h2 className="text-lg font-semibold">Attached services</h2>
@ -598,10 +600,11 @@ export function ProjectWorkspaceDetail() {
Shared services for this project workspace. Execution workspaces inherit this config unless they override it. Shared services for this project workspace. Execution workspaces inherit this config unless they override it.
</p> </p>
</div> </div>
<div className="flex shrink-0 flex-wrap gap-2"> <div className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:flex-wrap">
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="w-full sm:w-auto"
disabled={controlRuntimeServices.isPending || !workspace.runtimeConfig?.workspaceRuntime || !workspace.cwd} disabled={controlRuntimeServices.isPending || !workspace.runtimeConfig?.workspaceRuntime || !workspace.cwd}
onClick={() => controlRuntimeServices.mutate("start")} onClick={() => controlRuntimeServices.mutate("start")}
> >
@ -611,6 +614,7 @@ export function ProjectWorkspaceDetail() {
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="w-full sm:w-auto"
disabled={controlRuntimeServices.isPending || !workspace.cwd} disabled={controlRuntimeServices.isPending || !workspace.cwd}
onClick={() => controlRuntimeServices.mutate("restart")} onClick={() => controlRuntimeServices.mutate("restart")}
> >
@ -619,6 +623,7 @@ export function ProjectWorkspaceDetail() {
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="w-full sm:w-auto"
disabled={controlRuntimeServices.isPending || (workspace.runtimeServices?.length ?? 0) === 0} disabled={controlRuntimeServices.isPending || (workspace.runtimeServices?.length ?? 0) === 0}
onClick={() => controlRuntimeServices.mutate("stop")} onClick={() => controlRuntimeServices.mutate("stop")}
> >
@ -631,7 +636,7 @@ export function ProjectWorkspaceDetail() {
<div className="space-y-3"> <div className="space-y-3">
{workspace.runtimeServices.map((service) => ( {workspace.runtimeServices.map((service) => (
<div key={service.id} className="rounded-xl border border-border/80 bg-background px-3 py-2"> <div key={service.id} className="rounded-xl border border-border/80 bg-background px-3 py-2">
<div className="flex items-start justify-between gap-3"> <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="space-y-1"> <div className="space-y-1">
<div className="text-sm font-medium">{service.serviceName}</div> <div className="text-sm font-medium">{service.serviceName}</div>
<div className="space-y-1 text-xs text-muted-foreground"> <div className="space-y-1 text-xs text-muted-foreground">
@ -646,7 +651,7 @@ export function ProjectWorkspaceDetail() {
{service.cwd ? <div className="break-all font-mono">{service.cwd}</div> : null} {service.cwd ? <div className="break-all font-mono">{service.cwd}</div> : null}
</div> </div>
</div> </div>
<div className="text-[11px] uppercase tracking-[0.14em] text-muted-foreground"> <div className="text-[11px] uppercase tracking-[0.14em] text-muted-foreground sm:text-right">
{service.status} · {service.healthStatus} {service.status} · {service.healthStatus}
</div> </div>
</div> </div>