diff --git a/frontend/src/App.css b/frontend/src/App.css index d2350a4..64daacc 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -44,12 +44,123 @@ body { .app-header { background: var(--surface); border-bottom: 1px solid var(--border); - padding: 14px 24px; + padding: 0 20px; + display: flex; + align-items: center; + gap: 16px; + height: 52px; + flex-shrink: 0; +} + +.app-header h1 { + font-size: 16px; + font-weight: 700; + white-space: nowrap; + flex-shrink: 0; +} + +/* Primary nav tabs in the top bar */ +.nav-tabs { + display: flex; + align-items: stretch; + gap: 2px; + flex: 1; + height: 100%; +} + +.nav-tab { + background: none; + border: none; + border-bottom: 3px solid transparent; + padding: 0 14px; + font-size: 13px; + font-weight: 500; + color: var(--text-muted); + cursor: pointer; + white-space: nowrap; + transition: color 0.15s, border-color 0.15s; + height: 100%; display: flex; align-items: center; } -.app-header h1 { font-size: 18px; font-weight: 600; } +.nav-tab:hover { color: var(--text); } + +.nav-tab.active { + color: var(--accent); + border-bottom-color: var(--accent); +} + +/* User menu dropdown */ +.user-menu-wrapper { + position: relative; +} + +.user-menu-trigger { + display: flex; + align-items: center; + gap: 7px; + background: none; + border: 1px solid var(--border); + border-radius: 20px; + padding: 4px 10px 4px 5px; + cursor: pointer; + color: var(--text); + font-size: 13px; + transition: background 0.15s; +} +.user-menu-trigger:hover { background: var(--hover); } + +.user-avatar { + width: 24px; + height: 24px; + border-radius: 50%; + background: var(--accent); + color: #fff; + font-size: 11px; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.user-menu-caret { font-size: 9px; color: var(--text-muted); } + +.user-menu-dropdown { + position: absolute; + top: calc(100% + 6px); + right: 0; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + box-shadow: 0 4px 16px rgba(0,0,0,0.12); + min-width: 160px; + z-index: 200; + overflow: hidden; + padding: 4px 0; +} + +.user-menu-item { + display: block; + width: 100%; + text-align: left; + background: none; + border: none; + padding: 8px 14px; + font-size: 13px; + color: var(--text); + cursor: pointer; +} +.user-menu-item:hover { background: var(--hover); } +.user-menu-item--danger { color: #e05252; } +.user-menu-item--danger:hover { background: rgba(224, 82, 82, 0.08); } + +.user-menu-divider { + height: 1px; + background: var(--border); + margin: 4px 0; +} .layout { display: flex; @@ -57,6 +168,11 @@ body { overflow: hidden; } +/* When no sidebar is present (Dashboard, Secrets, IPAM) */ +.layout.no-sidebar .main { + max-width: 1200px; +} + /* ===== Sidebar ===== */ .sidebar { width: 220px; diff --git a/frontend/src/App.js b/frontend/src/App.js index ff04ce2..2466fdc 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import { getProviders, getZones, getRecords, syncRecords, addRecord, updateRecord, deleteRecord, getMe, setToken } from './api/dns'; import RecordsTable from './components/RecordsTable'; import AddRecordForm from './components/AddRecordForm'; @@ -29,6 +29,12 @@ function formatSyncedAt(iso) { // ─── Main app (shown when authenticated) ───────────────────────────────────── function AppShell({ currentUser, onLogout }) { + // ── Top-level section nav ──────────────────────────────────────────────── + const [section, setSection] = useState('dashboard'); // dashboard|dns|secrets|ipam|admin + const [adminView, setAdminView] = useState('settings'); // settings|audit|diag|profile + const [dnsView, setDnsView] = useState('domains'); // domains | (provider is selected) + + // ── DNS state ──────────────────────────────────────────────────────────── const [providers, setProviders] = useState([]); const [selectedProvider, setSelectedProvider] = useState(null); const [zones, setZones] = useState([]); @@ -38,16 +44,40 @@ function AppShell({ currentUser, onLogout }) { const [loadingZones, setLoadingZones] = useState(false); const [loadingRecords, setLoadingRecords] = useState(false); const [syncing, setSyncing] = useState(false); - const [view, setView] = useState('dashboard'); const [showForm, setShowForm] = useState(false); const [editingRecord, setEditingRecord] = useState(null); - const { colors: providerColors } = useProviderColors(); - const { theme, toggleTheme } = useTheme(); const [deletingId, setDeletingId] = useState(null); const [confirmDelete, setConfirmDelete] = useState(null); - const [error, setError] = useState(''); const [filter, setFilter] = useState(''); + const { colors: providerColors } = useProviderColors(); + const { theme, toggleTheme } = useTheme(); + const [error, setError] = useState(''); + const [userMenuOpen, setUserMenuOpen] = useState(false); + const userMenuRef = useRef(null); + + // Close user menu on outside click + useEffect(() => { + function handler(e) { + if (userMenuRef.current && !userMenuRef.current.contains(e.target)) { + setUserMenuOpen(false); + } + } + document.addEventListener('mousedown', handler); + return () => document.removeEventListener('mousedown', handler); + }, []); + + function switchSection(s) { + setSection(s); + setSelectedProvider(null); + setSelectedZone(null); + setDnsView('domains'); + setFilter(''); + setError(''); + } + + const hasSidebar = section === 'dns' || section === 'admin'; + useEffect(() => { getProviders() .then(setProviders) @@ -83,6 +113,7 @@ function AppShell({ currentUser, onLogout }) { function selectZone(zone) { setSelectedZone(zone); setFilter(''); + setDnsView('provider'); loadRecords(selectedProvider, zone); } @@ -140,123 +171,146 @@ function AppShell({ currentUser, onLogout }) { return (