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 (
<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>
{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>
{children}
</label>
@ -172,8 +172,8 @@ function Field({
function DetailRow({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div className="flex items-start gap-3 py-1.5">
<div className="w-32 shrink-0 text-xs text-muted-foreground">{label}</div>
<div className="flex flex-col gap-1.5 py-1.5 sm:flex-row sm:items-start sm:gap-3">
<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>
);
@ -189,7 +189,7 @@ function StatusPill({ children, className }: { children: React.ReactNode; classN
function MonoValue({ value, copy }: { value: string; copy?: boolean }) {
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>
{copy ? (
<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="space-y-6">
<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="text-xs font-medium uppercase tracking-[0.16em] text-muted-foreground">
Execution workspace
@ -396,9 +396,10 @@ export function ExecutionWorkspaceDetail() {
and runtime-service behavior in sync with the actual workspace being reused.
</p>
</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
variant="outline"
className="w-full sm:w-auto"
onClick={() => setCloseDialogOpen(true)}
disabled={workspace.status === "archived"}
>
@ -496,7 +497,7 @@ export function ExecutionWorkspaceDetail() {
</Field>
<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 className="text-xs font-medium uppercase tracking-[0.16em] text-muted-foreground">
Runtime config source
@ -511,6 +512,7 @@ export function ExecutionWorkspaceDetail() {
</div>
<Button
variant="outline"
className="w-full sm:w-auto"
size="sm"
disabled={!linkedProjectWorkspace?.runtimeConfig?.workspaceRuntime}
onClick={() =>
@ -548,13 +550,14 @@ export function ExecutionWorkspaceDetail() {
</Field>
</div>
<div className="mt-5 flex flex-wrap items-center gap-3">
<Button disabled={!isDirty || updateWorkspace.isPending} onClick={saveChanges}>
<div className="mt-5 flex flex-col items-stretch gap-3 sm:flex-row sm:flex-wrap sm:items-center">
<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}
Save changes
</Button>
<Button
variant="outline"
className="w-full sm:w-auto"
disabled={!isDirty || updateWorkspace.isPending}
onClick={() => {
setForm(initialState);
@ -631,12 +634,12 @@ export function ExecutionWorkspaceDetail() {
</DetailRow>
<DetailRow label="Repo URL">
{workspace.repoUrl && isSafeExternalUrl(workspace.repoUrl) ? (
<div className="inline-flex items-center gap-2">
<a href={workspace.repoUrl} target="_blank" rel="noreferrer" className="inline-flex items-center gap-1 hover:underline">
<div className="inline-flex max-w-full items-start gap-2">
<a href={workspace.repoUrl} target="_blank" rel="noreferrer" className="inline-flex min-w-0 items-center gap-1 break-all hover:underline">
{workspace.repoUrl}
<ExternalLink className="h-3.5 w-3.5" />
<ExternalLink className="h-3.5 w-3.5 shrink-0" />
</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" />
</CopyText>
</div>
@ -662,7 +665,7 @@ export function ExecutionWorkspaceDetail() {
</div>
<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="text-xs font-medium uppercase tracking-[0.16em] text-muted-foreground">Runtime services</div>
<h2 className="text-lg font-semibold">Attached services</h2>
@ -674,10 +677,11 @@ export function ExecutionWorkspaceDetail() {
: "none"}
</p>
</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
variant="outline"
size="sm"
className="w-full sm:w-auto"
disabled={controlRuntimeServices.isPending || !effectiveRuntimeConfig || !workspace.cwd}
onClick={() => controlRuntimeServices.mutate("start")}
>
@ -687,6 +691,7 @@ export function ExecutionWorkspaceDetail() {
<Button
variant="outline"
size="sm"
className="w-full sm:w-auto"
disabled={controlRuntimeServices.isPending || !effectiveRuntimeConfig || !workspace.cwd}
onClick={() => controlRuntimeServices.mutate("restart")}
>
@ -695,6 +700,7 @@ export function ExecutionWorkspaceDetail() {
<Button
variant="outline"
size="sm"
className="w-full sm:w-auto"
disabled={controlRuntimeServices.isPending || (workspace.runtimeServices?.length ?? 0) === 0}
onClick={() => controlRuntimeServices.mutate("stop")}
>
@ -707,7 +713,7 @@ export function ExecutionWorkspaceDetail() {
<div className="space-y-3">
{workspace.runtimeServices.map((service) => (
<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="text-sm font-medium">{service.serviceName}</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}
</div>
</div>
<StatusPill>{service.healthStatus}</StatusPill>
<StatusPill className="self-start">{service.healthStatus}</StatusPill>
</div>
</div>
))}
@ -755,7 +761,7 @@ export function ExecutionWorkspaceDetail() {
<div className="space-y-3">
{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 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="text-sm font-medium">{operation.command ?? operation.phase}</div>
<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>
) : null}
</div>
<StatusPill>{operation.status}</StatusPill>
<StatusPill className="self-start">{operation.status}</StatusPill>
</div>
</div>
))}

View file

@ -186,9 +186,9 @@ function Field({
}) {
return (
<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>
{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>
{children}
</label>
@ -197,8 +197,8 @@ function Field({
function DetailRow({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div className="flex items-start gap-3 py-1.5">
<div className="w-28 shrink-0 text-xs text-muted-foreground">{label}</div>
<div className="flex flex-col gap-1.5 py-1.5 sm:flex-row sm:items-start sm:gap-3">
<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>
);
@ -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="space-y-6">
<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="text-xs font-medium uppercase tracking-[0.16em] text-muted-foreground">
Project workspace
@ -379,6 +379,7 @@ export function ProjectWorkspaceDetail() {
{!workspace.isPrimary ? (
<Button
variant="outline"
className="w-full sm:w-auto"
disabled={setPrimaryWorkspace.isPending}
onClick={() => setPrimaryWorkspace.mutate()}
>
@ -388,7 +389,7 @@ export function ProjectWorkspaceDetail() {
Make primary
</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" />
This is the projects primary codebase workspace.
</div>
@ -537,13 +538,14 @@ export function ProjectWorkspaceDetail() {
</Field>
</div>
<div className="mt-5 flex flex-wrap items-center gap-3">
<Button disabled={!isDirty || updateWorkspace.isPending} onClick={saveChanges}>
<div className="mt-5 flex flex-col items-stretch gap-3 sm:flex-row sm:flex-wrap sm:items-center">
<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}
Save changes
</Button>
<Button
variant="outline"
className="w-full sm:w-auto"
disabled={!isDirty || updateWorkspace.isPending}
onClick={() => {
setForm(initialState);
@ -590,7 +592,7 @@ export function ProjectWorkspaceDetail() {
</div>
<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="text-xs font-medium uppercase tracking-[0.16em] text-muted-foreground">Runtime services</div>
<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.
</p>
</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
variant="outline"
size="sm"
className="w-full sm:w-auto"
disabled={controlRuntimeServices.isPending || !workspace.runtimeConfig?.workspaceRuntime || !workspace.cwd}
onClick={() => controlRuntimeServices.mutate("start")}
>
@ -611,6 +614,7 @@ export function ProjectWorkspaceDetail() {
<Button
variant="outline"
size="sm"
className="w-full sm:w-auto"
disabled={controlRuntimeServices.isPending || !workspace.cwd}
onClick={() => controlRuntimeServices.mutate("restart")}
>
@ -619,6 +623,7 @@ export function ProjectWorkspaceDetail() {
<Button
variant="outline"
size="sm"
className="w-full sm:w-auto"
disabled={controlRuntimeServices.isPending || (workspace.runtimeServices?.length ?? 0) === 0}
onClick={() => controlRuntimeServices.mutate("stop")}
>
@ -631,7 +636,7 @@ export function ProjectWorkspaceDetail() {
<div className="space-y-3">
{workspace.runtimeServices.map((service) => (
<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="text-sm font-medium">{service.serviceName}</div>
<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}
</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}
</div>
</div>