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:
@@ -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">
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
justify-content: space-between;
|
||||
gap: var(--space-4);
|
||||
align-items: flex-start;
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.home-hero-actions {
|
||||
@@ -123,11 +124,19 @@
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
}
|
||||
|
||||
.home-summary-card {
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.home-project-grid,
|
||||
.home-session-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
}
|
||||
|
||||
.home-section {
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.home-section h2,
|
||||
.settings-header h1,
|
||||
.settings-panel h2 {
|
||||
@@ -139,6 +148,14 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.home-hero,
|
||||
.home-summary-card,
|
||||
.home-section {
|
||||
padding: var(--space-3);
|
||||
}
|
||||
}
|
||||
|
||||
.row-tight {
|
||||
gap: var(--space-2);
|
||||
}
|
||||
@@ -576,6 +593,49 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Shared config section — used by Tool Workshop and Config Profiles */
|
||||
.config-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-4);
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.config-section-title {
|
||||
margin: 0 0 var(--space-1);
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: 600;
|
||||
line-height: var(--line-height-tight);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.config-section-subtitle {
|
||||
margin: calc(var(--space-1) * -1) 0 var(--space-2);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--muted);
|
||||
line-height: var(--line-height-normal);
|
||||
}
|
||||
|
||||
.config-section > .config-section-title + .config-section-subtitle {
|
||||
margin-top: calc(var(--space-2) * -1);
|
||||
}
|
||||
|
||||
.config-section-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.config-section-header .config-section-title {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Legacy alias for existing consumers */
|
||||
.form-section {
|
||||
padding: var(--space-4);
|
||||
background: var(--panel);
|
||||
|
||||
@@ -45,24 +45,40 @@
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.project-repos {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.project-repos-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.project-repos-header .config-section-title {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.repo-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-4);
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.repo-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-4);
|
||||
background: var(--bg);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.repo-block,
|
||||
.repo-block.card-md {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.repo-header {
|
||||
@@ -139,11 +155,14 @@
|
||||
}
|
||||
|
||||
.project-add-repo {
|
||||
margin-top: var(--space-4);
|
||||
padding-top: var(--space-4);
|
||||
border-top: 1px solid var(--border);
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.project-repos-header {
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
}
|
||||
|
||||
/* Inline radio buttons for repository creation mode */
|
||||
|
||||
Reference in New Issue
Block a user