feat(web/ui): unify config sections, restore home padding, improve project repos

- Add shared .config-section component class and migrate Tool Workshop
  and Config Profiles editors to use it.
- Restore --space-4 padding on home hero, summary cards, and sections
  with --space-3 on mobile.
- Restructure expandable project card: add 'Repositories' header with
  repo count and move 'Add Repository' button into the section header.
- Left-align repository blocks so they fill from the project detail edge.
This commit is contained in:
Developer
2026-06-16 15:55:58 +00:00
parent cbd5533adf
commit 972dae64f2
5 changed files with 235 additions and 134 deletions
@@ -173,9 +173,12 @@ export const ConfigProfileEditorPanel = ({
</label>
</div>
<div className="form-section">
<div className="row justify-between items-center mb-3">
<h4 className="m-0">Includes</h4>
<section className="config-section">
<div className="config-section-header">
<div>
<h4 className="config-section-title">Includes</h4>
<p className="config-section-subtitle">Compose configurations from other profiles</p>
</div>
<span className="muted text-sm">{includedProfileIds.length} included</span>
</div>
{includedProfileIds.length === 0 ? (
@@ -216,10 +219,10 @@ export const ConfigProfileEditorPanel = ({
</select>
</div>
)}
</div>
</section>
<div className="form-section">
<h4 className="m-0 mb-3">Environment Variables</h4>
<section className="config-section">
<h4 className="config-section-title">Environment Variables</h4>
{Object.entries(formData.env_vars || {}).map(([key, value], idx) => (
<div key={idx} className="form-row row-sm mb-2">
<input type="text" value={key} onChange={(e) => onUpdateEnvVar(key, e.target.value, value)} placeholder="VAR_NAME" className="form-input" />
@@ -228,16 +231,20 @@ export const ConfigProfileEditorPanel = ({
</div>
))}
<button type="button" className="secondary-button" onClick={onAddEnvVar}><Icon name="add" size="sm" /> Add Variable</button>
</div>
</section>
<div className="form-section">
<h4 className="m-0 mb-3">Runtime Hints</h4>
<section className="config-section">
<h4 className="config-section-title">Runtime Hints</h4>
<textarea value={JSON.stringify(formData.runtime_hints || {}, null, 2)} onChange={(e) => { try { const parsed = JSON.parse(e.target.value); onFormChange("runtime_hints", parsed); } catch { /* ignore */ } }} placeholder='{"start_command": "npm start"}' rows={4} className="form-input font-mono text-sm" />
</div>
</section>
<div className="form-section">
<h4 className="m-0 mb-2">Files</h4>
<p className="muted text-sm m-0 mb-3">Relative paths written to the instance directory. Use Mounts below for absolute container paths.</p>
<section className="config-section">
<div className="config-section-header">
<div>
<h4 className="config-section-title">Files</h4>
<p className="config-section-subtitle">Relative paths written to the instance directory</p>
</div>
</div>
{Object.entries(formData.files || {}).map(([path, content], idx) => (
<div key={idx} className="card-sm mb-2">
<div className="form-row row-sm mb-2">
@@ -248,11 +255,15 @@ export const ConfigProfileEditorPanel = ({
</div>
))}
<button type="button" className="secondary-button" onClick={onAddFile}><Icon name="add" size="sm" /> Add File</button>
</div>
</section>
<div className="form-section">
<h4 className="m-0 mb-2">Mounts</h4>
<p className="muted text-sm m-0 mb-3">Bind directories into the container at absolute paths. Files are relative to the mount target.</p>
<section className="config-section">
<div className="config-section-header">
<div>
<h4 className="config-section-title">Mounts</h4>
<p className="config-section-subtitle">Bind directories into the container at absolute paths</p>
</div>
</div>
{(formData.mounts || []).map((mount, index) => (
<div key={index} className="card-sm mb-3">
<div className="form-row row-sm mb-3">
@@ -276,11 +287,12 @@ export const ConfigProfileEditorPanel = ({
</div>
))}
<button type="button" className="secondary-button" onClick={onAddMount}><Icon name="add" size="sm" /> Add Mount</button>
</div>
</section>
<div className="form-section">
<section className="config-section">
<h4 className="config-section-title">Git Mounts</h4>
<GitMountEditor mounts={formData.git_mounts || []} onChange={(git_mounts) => onFormChange("git_mounts", git_mounts)} />
</div>
</section>
<div className="dialog-actions sticky-footer">
<button type="submit" disabled={saveStatus === "saving"} className="btn btn-primary">
@@ -119,89 +119,99 @@ export const ProjectCard = ({
{expanded && (
<div className="project-detail">
{(project.repositories || []).length === 0 ? (
<p className="muted">No repositories yet.</p>
) : (
<div className="repo-list">
{(project.repositories || []).map((repo) => (
<div key={repo.id} className="card card-md repo-block">
<div className="repo-header">
<h4>{repo.name}</h4>
<button
className="btn btn-sm btn-primary"
onClick={() => onCreateWorkspace(repo.id)}
type="button"
>
<Icon name="add" size="sm" /> New Workspace
</button>
</div>
{showCreateForm === repo.id && (
<WorkspaceCreateForm
defaultProjectId={project.id}
defaultRepoId={repo.id}
onSubmit={onCreated}
onCancel={onCancelCreate}
/>
)}
{repo.workspaces.length === 0 ? (
<p className="muted">No workspaces.</p>
) : (
<div className="workspace-grid">
{repo.workspaces.map((ws) => (
<div
key={ws.id}
className={`workspace-chip ${ws.status}`}
>
<a href={`/workspaces/${ws.id}`}>{ws.name}</a>
<span className="ws-branch">
<Icon name="branch" size="sm" /> {ws.branch}
</span>
{ws.instance_count > 0 && (
<span className="ws-instances">
{ws.instance_count} tool
{ws.instance_count > 1 ? "s" : ""}
</span>
)}
<div className="ws-actions">
<button
type="button"
disabled={workspaceLoading === ws.id}
onClick={() =>
onWorkspaceAction(repo.id, ws, "sync")
}
>
<Icon name="refresh" size="sm" />
</button>
<button
type="button"
className="danger-text"
disabled={workspaceLoading === ws.id}
onClick={() =>
onWorkspaceAction(repo.id, ws, "delete")
}
>
<Icon name="delete" size="sm" />
</button>
</div>
</div>
))}
<section className="project-repos">
<div className="project-repos-header">
<div>
<h4 className="config-section-title">Repositories</h4>
<p className="config-section-subtitle">
{project.repositories?.length ?? 0} repo
{(project.repositories?.length ?? 0) !== 1 ? "s" : ""}
</p>
</div>
{onAddRepository && (
<button
className="btn btn-sm btn-secondary"
onClick={onAddRepository}
type="button"
>
<Icon name="add" size="sm" /> Add Repository
</button>
)}
</div>
{(project.repositories || []).length === 0 ? (
<p className="muted">No repositories yet.</p>
) : (
<div className="repo-list">
{(project.repositories || []).map((repo) => (
<div key={repo.id} className="card card-md repo-block">
<div className="repo-header">
<h4>{repo.name}</h4>
<button
className="btn btn-sm btn-primary"
onClick={() => onCreateWorkspace(repo.id)}
type="button"
>
<Icon name="add" size="sm" /> New Workspace
</button>
</div>
)}
</div>
))}
</div>
)}
{onAddRepository && (
<div className="project-add-repo">
<button
className="primary-button"
onClick={onAddRepository}
type="button"
>
<Icon name="add" size="sm" /> Add Repository
</button>
</div>
)}
{showCreateForm === repo.id && (
<WorkspaceCreateForm
defaultProjectId={project.id}
defaultRepoId={repo.id}
onSubmit={onCreated}
onCancel={onCancelCreate}
/>
)}
{repo.workspaces.length === 0 ? (
<p className="muted">No workspaces.</p>
) : (
<div className="workspace-grid">
{repo.workspaces.map((ws) => (
<div
key={ws.id}
className={`workspace-chip ${ws.status}`}
>
<a href={`/workspaces/${ws.id}`}>{ws.name}</a>
<span className="ws-branch">
<Icon name="branch" size="sm" /> {ws.branch}
</span>
{ws.instance_count > 0 && (
<span className="ws-instances">
{ws.instance_count} tool
{ws.instance_count > 1 ? "s" : ""}
</span>
)}
<div className="ws-actions">
<button
type="button"
disabled={workspaceLoading === ws.id}
onClick={() =>
onWorkspaceAction(repo.id, ws, "sync")
}
>
<Icon name="refresh" size="sm" />
</button>
<button
type="button"
className="danger-text"
disabled={workspaceLoading === ws.id}
onClick={() =>
onWorkspaceAction(repo.id, ws, "delete")
}
>
<Icon name="delete" size="sm" />
</button>
</div>
</div>
))}
</div>
)}
</div>
))}
</div>
)}
</section>
</div>
)}
</article>
@@ -286,8 +286,8 @@ export const ManifestEditor = ({
return (
<div className="stack stack-lg">
{/* Base Image */}
<div className="card-md stack stack-md">
<h4 className="m-0">Base Image</h4>
<section className="config-section">
<h4 className="config-section-title">Base Image</h4>
<div className="form-row row-md">
<div className="form-group">
<label>Base Definition</label>
@@ -322,11 +322,11 @@ export const ManifestEditor = ({
/>
</div>
</div>
</div>
</section>
{/* Packages */}
<div className="card-md stack stack-md">
<h4 className="m-0">Packages</h4>
<section className="config-section">
<h4 className="config-section-title">Packages</h4>
<div className="form-group">
<label>Node.js Version</label>
@@ -435,11 +435,11 @@ export const ManifestEditor = ({
</button>
</div>
</div>
</div>
</section>
{/* Runtime User */}
<div className="card-md stack stack-md">
<h4 className="m-0">Runtime User</h4>
<section className="config-section">
<h4 className="config-section-title">Runtime User</h4>
<div className="form-row row-md">
<div className="form-group">
<label>User Name</label>
@@ -469,11 +469,11 @@ export const ManifestEditor = ({
/>
</div>
</div>
</div>
</section>
{/* Environment */}
<div className="card-md stack stack-md">
<h4 className="m-0">Environment Variables</h4>
<section className="config-section">
<h4 className="config-section-title">Environment Variables</h4>
<div className="stack stack-sm">
{envVars.map((ev, idx) => (
<div key={idx} className="form-row row-sm">
@@ -509,11 +509,11 @@ export const ManifestEditor = ({
<Icon name="add" size="sm" /> Add Env Var
</button>
</div>
</div>
</section>
{/* Build Scripts */}
<div className="card-md stack stack-md">
<h4 className="m-0">Build Scripts (run during docker build)</h4>
<section className="config-section">
<h4 className="config-section-title">Build Scripts (run during docker build)</h4>
<div className="stack stack-sm">
{buildScripts.map((script, idx) => (
<div
@@ -545,11 +545,11 @@ export const ManifestEditor = ({
<Icon name="add" size="sm" /> Add Build Script
</button>
</div>
</div>
</section>
{/* Startup Scripts */}
<div className="card-md stack stack-md">
<h4 className="m-0">
<section className="config-section">
<h4 className="config-section-title">
Startup Scripts (run when container starts)
</h4>
<div className="stack stack-sm">
@@ -583,11 +583,11 @@ export const ManifestEditor = ({
<Icon name="add" size="sm" /> Add Startup Script
</button>
</div>
</div>
</section>
{/* Mounts */}
<div className="card-md stack stack-md">
<h4 className="m-0">Mount Schema</h4>
<section className="config-section">
<h4 className="config-section-title">Mount Schema</h4>
<div className="stack stack-md">
{mounts.map((mount, idx) => (
<div
@@ -701,11 +701,11 @@ export const ManifestEditor = ({
<Icon name="add" size="sm" /> Add Mount
</button>
</div>
</div>
</section>
{/* Runtime */}
<div className="card-md stack stack-md">
<h4 className="m-0">Runtime</h4>
<section className="config-section">
<h4 className="config-section-title">Runtime</h4>
<div className="form-row row-md">
<div className="form-group">
<label>Command</label>
@@ -746,7 +746,7 @@ export const ManifestEditor = ({
tty
</label>
</div>
</div>
</section>
{/* Preview */}
<div className="card-md stack stack-md">