feat: redesign workspace card header and layout

- Swap workspace card header order: project name is now the primary eyebrow,
  workspace name is the bold title below it.
- Restructure workspace card into clean top/body/actions sections with more
  whitespace and clearer hierarchy.
- Replace cramped meta paragraphs with an inline meta row (repo, branch,
  instance count) and dedicated instance chip area.
- Use icon-only ghost buttons for sync/delete to reduce visual noise; keep
  prominent Start Tool button.
- Add top divider for actions, improve hover states, and make long names
  truncate gracefully.
- Update mobile workspace list subtitle to project · workspace name.
- Refresh workspaces.css with new card layout and responsive mobile rules.

Quality gates: npm run typecheck clean, npm run lint clean,
npm test -- --run 87 passed
This commit is contained in:
Developer
2026-06-13 17:00:16 +00:00
parent 2b5b8363ca
commit 4e1477c4be
3 changed files with 182 additions and 51 deletions
@@ -29,47 +29,68 @@ export function WorkspaceCard({
return ( return (
<article className={`card workspace-card ${loading ? "loading" : ""}`}> <article className={`card workspace-card ${loading ? "loading" : ""}`}>
<Link <div className="workspace-card-top">
to={`/workspaces/${workspace.id}`} <Link
className="workspace-header-link" to={`/workspaces/${workspace.id}`}
> className="workspace-title-link"
<div className="workspace-header"> >
<h4>{workspace.name}</h4> <div className="workspace-title-stack">
<span className={`status-badge ${statusClass}`}> <span className="workspace-project-name">
{workspace.status} {workspace.project_name}
</span>
<h4 className="workspace-name">{workspace.name}</h4>
</div>
</Link>
<span className={`status-badge ${statusClass}`}>
{workspace.status}
</span>
</div>
<div className="workspace-card-body">
<div className="workspace-meta-row">
<span className="workspace-repo">
<Icon name="repositories" size="sm" />
{workspace.repo_name}
</span> </span>
<span className="workspace-branch">
<Icon name="branch" size="sm" />
{workspace.branch}
</span>
{workspace.instance_count > 0 && (
<span className="workspace-instance-count">
{workspace.instance_count} tool
{workspace.instance_count > 1 ? "s" : ""}
</span>
)}
</div> </div>
</Link>
<div className="workspace-meta">
<p className="workspace-project">
{workspace.project_name} / {workspace.repo_name}
</p>
<p className="workspace-branch">
<Icon name="branch" size="sm" /> {workspace.branch}
</p>
<WorkspaceInstanceChips workspaceId={workspace.id} /> <WorkspaceInstanceChips workspaceId={workspace.id} />
</div> </div>
<div className="workspace-actions"> <div className="workspace-actions">
<button <button
className="btn btn-primary" className="btn btn-primary"
onClick={() => onStartTool(workspace)} onClick={() => onStartTool(workspace)}
disabled={loading} disabled={loading}
title="Start a tool in this workspace"
> >
<Icon name="play" size="sm" /> Start Tool <Icon name="play" size="sm" />
Start Tool
</button> </button>
<button <button
className="btn btn-secondary" className="ghost-button"
onClick={() => onSync(workspace)} onClick={() => onSync(workspace)}
disabled={loading} disabled={loading}
title="Sync workspace"
> >
<Icon name="refresh" size="sm" /> Sync <Icon name="refresh" size="sm" />
</button> </button>
<button <button
className="btn btn-danger" className="ghost-button danger-text"
onClick={() => onDelete(workspace)} onClick={() => onDelete(workspace)}
disabled={loading} disabled={loading}
title="Delete workspace"
> >
<Icon name="delete" size="sm" /> Delete <Icon name="delete" size="sm" />
</button> </button>
</div> </div>
</article> </article>
+1 -1
View File
@@ -104,7 +104,7 @@ export function WorkspacesPage() {
items={workspaces.map((ws) => ({ items={workspaces.map((ws) => ({
id: ws.id, id: ws.id,
title: ws.name, title: ws.name,
subtitle: `${ws.project_name} · ${ws.repo_name} · ${ws.branch}`, subtitle: `${ws.project_name} · ${ws.name}`,
status: ws.status, status: ws.status,
}))} }))}
onItemClick={(id) => { onItemClick={(id) => {
+139 -29
View File
@@ -7,22 +7,28 @@
.workspaces-grid { .workspaces-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
gap: var(--space-4); gap: var(--space-4);
margin-top: var(--space-4); margin-top: var(--space-4);
} }
/* Workspace card */ /* Workspace card */
.workspace-card { .workspace-card {
padding: var(--space-4); display: flex;
flex-direction: column;
gap: var(--space-4);
padding: var(--space-5);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 12px; border-radius: 12px;
background: var(--panel); background: var(--panel);
transition: box-shadow 0.15s ease; transition:
box-shadow 0.15s ease,
border-color 0.15s ease;
} }
.workspace-card:hover { .workspace-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); box-shadow: 0 6px 18px rgba(0, 0, 0, 0.08);
border-color: color-mix(in srgb, var(--border) 80%, var(--brand));
} }
.workspace-card.loading { .workspace-card.loading {
@@ -30,46 +36,149 @@
pointer-events: none; pointer-events: none;
} }
.workspace-header-link { /* Top row: title + status */
.workspace-card-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--space-3);
}
.workspace-title-link {
display: block; display: block;
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
}
.workspace-header {
display: flex;
align-items: center;
gap: var(--space-2);
margin-bottom: var(--space-2);
}
.workspace-header h4 {
margin: 0;
font-size: 1.1rem;
flex: 1; flex: 1;
min-width: 0;
} }
.workspace-meta { .workspace-title-link:hover .workspace-name {
margin-bottom: var(--space-3); color: var(--brand);
} }
.workspace-meta p { .workspace-title-stack {
margin: 0.15rem 0; display: flex;
font-size: 0.875rem; flex-direction: column;
gap: var(--space-1);
min-width: 0;
}
.workspace-project-name {
font-size: var(--font-size-sm);
font-weight: 500;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.04em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.workspace-name {
margin: 0;
font-size: var(--font-size-lg);
font-weight: 700;
color: var(--ink);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Body: meta row + instance chips */
.workspace-card-body {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.workspace-meta-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-3);
font-size: var(--font-size-sm);
color: var(--muted); color: var(--muted);
} }
.workspace-meta-row > span {
display: inline-flex;
align-items: center;
gap: var(--space-1);
}
.workspace-repo {
color: var(--muted);
}
.workspace-branch {
color: var(--muted);
}
.workspace-instance-count {
padding: var(--space-1) var(--space-2);
background: var(--bg);
border-radius: 999px;
font-size: var(--font-size-xs);
font-weight: 500;
color: var(--success);
}
/* Instance chips */
.instance-chips {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
}
.instance-chip {
display: inline-flex;
align-items: center;
gap: var(--space-1);
font-size: var(--font-size-xs);
padding: var(--space-1) var(--space-2);
background: var(--bg);
border: 1px solid var(--border);
border-radius: 999px;
color: var(--ink);
max-width: 100%;
}
.instance-chip.running {
background: var(--success-light);
border-color: color-mix(in srgb, var(--success) 25%, transparent);
color: var(--success);
}
.instance-chip a {
color: inherit;
text-decoration: none;
margin-left: var(--space-1);
}
.instance-chip a:hover {
text-decoration: underline;
}
/* Actions */
.workspace-actions { .workspace-actions {
display: flex; display: flex;
align-items: center;
gap: var(--space-2); gap: var(--space-2);
flex-wrap: wrap; margin-top: auto;
padding-top: var(--space-3);
border-top: 1px solid var(--border);
}
.workspace-actions .btn-primary {
margin-right: auto;
} }
/* Status badges */ /* Status badges */
.status-badge { .status-badge {
font-size: 0.75rem; flex-shrink: 0;
font-size: var(--font-size-xs);
font-weight: 600; font-weight: 600;
padding: 0.15rem 0.5rem; padding: var(--space-1) var(--space-2);
border-radius: 999px; border-radius: 999px;
text-transform: capitalize; text-transform: capitalize;
} }
@@ -103,7 +212,7 @@
/* Mobile */ /* Mobile */
@media (max-width: 767px) { @media (max-width: 767px) {
.workspaces-page { .workspaces-page {
padding: var(--space-2); padding: var(--space-3);
} }
.workspaces-grid { .workspaces-grid {
@@ -112,14 +221,15 @@
} }
.workspace-card { .workspace-card {
padding: var(--space-3); padding: var(--space-4);
gap: var(--space-3);
} }
.workspace-actions { .workspace-actions {
flex-direction: column; flex-wrap: wrap;
} }
.workspace-actions .btn { .workspace-actions .btn-primary {
width: 100%; width: 100%;
justify-content: center; justify-content: center;
} }