redesign(services): WordPress-style grouped Settings sub-nav

Restructure the service Settings tab from a flat stack into a grouped left
sub-nav + content pane (.svc-settings grid), mirroring the WordPress detail
page: General -> Environment, Connections -> Repository, Advanced -> Danger
Zone, each with an icon. Section is local state; the existing panels are
unchanged, only reorganized.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Juan DenisandClaude Opus 4.8 committed 2026-06-21 00:19:20 -04:00
1 parent 6fb0454ac6
commit e46c0d685e
2 files changed
+195 -87

No files matched your search

@@ -1,14 +1,26 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { SlidersHorizontal, GitBranch, AlertTriangle } from 'lucide-react';
import api from '../../services/api';
import { useToast } from '../../contexts/ToastContext';
import { DangerZone } from '../DangerZone';
import { Button } from '@/components/ui/button';
import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '@/components/ui/select';
// Grouped left sub-nav for the service Settings tab — mirrors the WordPress
// detail page's settings layout: an uppercase mono group label per section with
// the existing setting panels on the right. Groups give it structure (and room
// to grow) instead of one long flat stack.
const SVC_SETTINGS_GROUPS = [
{ label: 'General', items: [{ id: 'environment', label: 'Environment', icon: SlidersHorizontal }] },
{ label: 'Connections', items: [{ id: 'repository', label: 'Repository', icon: GitBranch }] },
{ label: 'Advanced', items: [{ id: 'danger', label: 'Danger Zone', icon: AlertTriangle }] },
];
const SettingsTab = ({ app, deployConfig, onUpdate, onOpenGitModal }) => {
const navigate = useNavigate();
const toast = useToast();
const [section, setSection] = useState('environment');
const [deleting, setDeleting] = useState(false);
const [environmentType, setEnvironmentType] = useState(app.environment_type || 'standalone');
const [savingEnvironment, setSavingEnvironment] = useState(false);
@@ -67,100 +79,127 @@ const SettingsTab = ({ app, deployConfig, onUpdate, onOpenGitModal }) => {
return (
<div className="svc-settings">
{/* Repository Configuration */}
<div className="svc-settings__section">
<h3 className="svc-settings__section-title">Repository</h3>
<div className="card">
<div className="settings-row">
<div className="settings-label">
<span>Connected Repository</span>
<span className="settings-hint">
{deployConfig
? `${deployConfig.repo_url} (${deployConfig.branch || 'main'})`
: 'No repository connected'}
</span>
</div>
<div className="settings-control">
<Button variant="outline" onClick={onOpenGitModal}>
{deployConfig ? 'Edit' : 'Connect'}
</Button>
</div>
<nav className="svc-settings__nav" aria-label="Service settings sections">
{SVC_SETTINGS_GROUPS.map(g => (
<div className="svc-settings__group" key={g.label}>
<div className="svc-settings__grouplabel">{g.label}</div>
{g.items.map(s => (
<button
type="button"
key={s.id}
className={`svc-settings__navitem ${section === s.id ? 'is-active' : ''}`}
onClick={() => setSection(s.id)}
>
<s.icon size={15} />
{s.label}
</button>
))}
</div>
</div>
</div>
))}
</nav>
{/* Environment Configuration */}
<div className="svc-settings__section">
<h3 className="svc-settings__section-title">Environment</h3>
<div className="card settings-section">
<div className="settings-row">
<div className="settings-label">
<span>Environment Type</span>
<span className="settings-hint">
{app.has_linked_app
? 'This app is linked. Unlink to change environment type.'
: 'Set how this application is used in your workflow.'}
</span>
</div>
<div className="settings-control">
{app.has_linked_app ? (
<span className={`env-badge env-${app.environment_type}`}>
{envLabels[app.environment_type] || app.environment_type}
</span>
) : (
<Select
value={environmentType}
onValueChange={handleEnvironmentChange}
disabled={savingEnvironment}
>
<SelectTrigger className="settings-select">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="standalone">Standalone</SelectItem>
<SelectItem value="development">Development</SelectItem>
<SelectItem value="staging">Staging</SelectItem>
<SelectItem value="production">Production</SelectItem>
</SelectContent>
</Select>
<div className="svc-settings__content">
{/* Environment Configuration */}
{section === 'environment' && (
<div className="svc-settings__section">
<h3 className="svc-settings__section-title">Environment</h3>
<div className="card settings-section">
<div className="settings-row">
<div className="settings-label">
<span>Environment Type</span>
<span className="settings-hint">
{app.has_linked_app
? 'This app is linked. Unlink to change environment type.'
: 'Set how this application is used in your workflow.'}
</span>
</div>
<div className="settings-control">
{app.has_linked_app ? (
<span className={`env-badge env-${app.environment_type}`}>
{envLabels[app.environment_type] || app.environment_type}
</span>
) : (
<Select
value={environmentType}
onValueChange={handleEnvironmentChange}
disabled={savingEnvironment}
>
<SelectTrigger className="settings-select">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="standalone">Standalone</SelectItem>
<SelectItem value="development">Development</SelectItem>
<SelectItem value="staging">Staging</SelectItem>
<SelectItem value="production">Production</SelectItem>
</SelectContent>
</Select>
)}
{savingEnvironment && <span className="settings-saving">Saving...</span>}
</div>
</div>
{app.has_linked_app && (
<div className="settings-row">
<div className="settings-label">
<span>Linked Application</span>
<span className="settings-hint">
Unlinking will reset both apps to standalone mode.
</span>
</div>
<div className="settings-control">
<Button
variant="outline"
onClick={handleUnlink}
disabled={unlinking}
>
{unlinking ? 'Unlinking...' : 'Unlink'}
</Button>
</div>
</div>
)}
{savingEnvironment && <span className="settings-saving">Saving...</span>}
</div>
</div>
)}
{app.has_linked_app && (
<div className="settings-row">
<div className="settings-label">
<span>Linked Application</span>
<span className="settings-hint">
Unlinking will reset both apps to standalone mode.
</span>
</div>
<div className="settings-control">
<Button
variant="outline"
onClick={handleUnlink}
disabled={unlinking}
>
{unlinking ? 'Unlinking...' : 'Unlink'}
</Button>
{/* Repository Configuration */}
{section === 'repository' && (
<div className="svc-settings__section">
<h3 className="svc-settings__section-title">Repository</h3>
<div className="card">
<div className="settings-row">
<div className="settings-label">
<span>Connected Repository</span>
<span className="settings-hint">
{deployConfig
? `${deployConfig.repo_url} (${deployConfig.branch || 'main'})`
: 'No repository connected'}
</span>
</div>
<div className="settings-control">
<Button variant="outline" onClick={onOpenGitModal}>
{deployConfig ? 'Edit' : 'Connect'}
</Button>
</div>
</div>
</div>
)}
</div>
</div>
</div>
)}
{/* Danger Zone */}
<div className="svc-settings__section">
<h3 className="svc-settings__section-title">Danger Zone</h3>
<DangerZone
description="Once you delete a service, there is no going back. All data will be permanently removed."
action={
<Button variant="destructive" onClick={handleDelete} disabled={deleting}>
{deleting ? 'Deleting...' : 'Delete Service'}
</Button>
}
/>
{/* Danger Zone */}
{section === 'danger' && (
<div className="svc-settings__section">
<h3 className="svc-settings__section-title">Danger Zone</h3>
<DangerZone
description="Once you delete a service, there is no going back. All data will be permanently removed."
action={
<Button variant="destructive" onClick={handleDelete} disabled={deleting}>
{deleting ? 'Deleting...' : 'Delete Service'}
</Button>
}
/>
</div>
)}
</div>
</div>
);
+71 -2
View File
@@ -719,12 +719,81 @@ h3.svc-eyebrow {
}
}
// Settings tab within service detail
// Settings tab within service detail — grouped left sub-nav + content pane,
// mirroring the WordPress detail page's settings layout (.wp-settings).
.svc-settings {
max-width: 820px;
display: grid;
grid-template-columns: 200px 1fr;
gap: $space-4;
align-items: start;
@media (max-width: $breakpoint-md) {
grid-template-columns: 1fr;
}
&__nav {
display: flex;
flex-direction: column;
gap: 2px;
position: sticky;
top: $space-4;
@media (max-width: $breakpoint-md) {
position: static;
flex-direction: row;
flex-wrap: wrap;
}
}
// Grouped left nav — an uppercase mono label per group.
&__group + &__group {
margin-top: $space-4;
}
&__grouplabel {
padding: 0 12px;
margin-bottom: 6px;
font-family: var(--mono);
font-size: 10px;
font-weight: $font-weight-semibold;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-faint);
}
&__navitem {
display: flex;
align-items: center;
gap: 9px;
padding: 9px 12px;
border: none;
border-radius: 8px;
background: none;
color: var(--text-dim);
font-size: 13.5px;
font-weight: $font-weight-medium;
text-align: left;
cursor: pointer;
transition: background 0.14s, color 0.14s;
svg { color: var(--text-faint); flex-shrink: 0; }
&:hover { background: var(--surface-hover); color: var(--text); }
&.is-active {
background: var(--accent-bg);
color: var(--accent-bright);
svg { color: var(--accent-bright); }
}
}
&__content {
min-width: 0;
}
&__section {
margin-bottom: $space-6;
&:last-child { margin-bottom: 0; }
}
&__section-title {