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 (
<article className={`card workspace-card ${loading ? "loading" : ""}`}>
<Link
to={`/workspaces/${workspace.id}`}
className="workspace-header-link"
>
<div className="workspace-header">
<h4>{workspace.name}</h4>
<span className={`status-badge ${statusClass}`}>
{workspace.status}
<div className="workspace-card-top">
<Link
to={`/workspaces/${workspace.id}`}
className="workspace-title-link"
>
<div className="workspace-title-stack">
<span className="workspace-project-name">
{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 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>
</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} />
</div>
<div className="workspace-actions">
<button
className="btn btn-primary"
onClick={() => onStartTool(workspace)}
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
className="btn btn-secondary"
className="ghost-button"
onClick={() => onSync(workspace)}
disabled={loading}
title="Sync workspace"
>
<Icon name="refresh" size="sm" /> Sync
<Icon name="refresh" size="sm" />
</button>
<button
className="btn btn-danger"
className="ghost-button danger-text"
onClick={() => onDelete(workspace)}
disabled={loading}
title="Delete workspace"
>
<Icon name="delete" size="sm" /> Delete
<Icon name="delete" size="sm" />
</button>
</div>
</article>
+1 -1
View File
@@ -104,7 +104,7 @@ export function WorkspacesPage() {
items={workspaces.map((ws) => ({
id: ws.id,
title: ws.name,
subtitle: `${ws.project_name} · ${ws.repo_name} · ${ws.branch}`,
subtitle: `${ws.project_name} · ${ws.name}`,
status: ws.status,
}))}
onItemClick={(id) => {
+139 -29
View File
@@ -7,22 +7,28 @@
.workspaces-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);
margin-top: var(--space-4);
}
/* 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-radius: 12px;
background: var(--panel);
transition: box-shadow 0.15s ease;
transition:
box-shadow 0.15s ease,
border-color 0.15s ease;
}
.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 {
@@ -30,46 +36,149 @@
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;
text-decoration: none;
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;
min-width: 0;
}
.workspace-meta {
margin-bottom: var(--space-3);
.workspace-title-link:hover .workspace-name {
color: var(--brand);
}
.workspace-meta p {
margin: 0.15rem 0;
font-size: 0.875rem;
.workspace-title-stack {
display: flex;
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);
}
.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 {
display: flex;
align-items: center;
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-badge {
font-size: 0.75rem;
flex-shrink: 0;
font-size: var(--font-size-xs);
font-weight: 600;
padding: 0.15rem 0.5rem;
padding: var(--space-1) var(--space-2);
border-radius: 999px;
text-transform: capitalize;
}
@@ -103,7 +212,7 @@
/* Mobile */
@media (max-width: 767px) {
.workspaces-page {
padding: var(--space-2);
padding: var(--space-3);
}
.workspaces-grid {
@@ -112,14 +221,15 @@
}
.workspace-card {
padding: var(--space-3);
padding: var(--space-4);
gap: var(--space-3);
}
.workspace-actions {
flex-direction: column;
flex-wrap: wrap;
}
.workspace-actions .btn {
.workspace-actions .btn-primary {
width: 100%;
justify-content: center;
}