From c44152154f01f11590086c85b0757d07cf7dd260 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Tue, 24 Mar 2026 04:25:17 +0100 Subject: [PATCH] ui(step-0): rewrite sidebar/shell to match reference admin panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AdminSidebar: flat nav list, orange right-border active indicator, ACTIVE pill badge, ChevronLeft collapse (rotates 180° when collapsed) - AdminShell: h-16 header with logo+title left, Bell+avatar+logout right, sidebar collapse state, bg-gray-50 main area - app.css: shared classes — data-table (navy header), table-card, btn-primary (navy), btn-secondary, search-input, action-btn, status-badge variants, page-title Co-Authored-By: Claude Sonnet 4.6 --- src/app.css | 167 ++++++++++++++++++++++++++ src/components/AdminShell.tsx | 193 +++++++++++++++--------------- src/components/AdminSidebar.tsx | 206 +++++++++++++++++--------------- 3 files changed, 373 insertions(+), 193 deletions(-) diff --git a/src/app.css b/src/app.css index bde1bde..f19b75c 100644 --- a/src/app.css +++ b/src/app.css @@ -88,6 +88,173 @@ body { .auth-inline-msg { margin-top: 12px; } .hint { margin: 6px 0 0; color: #64748b; font-size: 12px; } +/* ===== Shared Page Components ===== */ + +/* Page header */ +.page-title { font-size: 1.5rem; font-weight: 700; color: #0a1d37; line-height: 1.2; } +.page-subtitle { font-size: 0.875rem; color: #64748b; margin-top: 4px; } + +/* Data table */ +.data-table { width: 100%; border-collapse: collapse; } +.data-table thead th { + background: #0a1d37; + color: rgba(255,255,255,0.9); + font-size: 0.75rem; + font-weight: 700; + text-align: left; + padding: 11px 14px; + white-space: nowrap; + user-select: none; +} +.data-table thead th:first-child { border-radius: 0; } +.data-table thead th:last-child { border-radius: 0; } +.data-table tbody td { + padding: 12px 14px; + font-size: 0.8125rem; + color: #0f172a; + vertical-align: middle; + border-bottom: 1px solid #f1f5f9; +} +.data-table tbody tr:last-child td { border-bottom: none; } +.data-table tbody tr:hover td { background: #fafbff; } +.data-table-empty { + text-align: center; + padding: 32px 16px; + font-size: 0.875rem; + color: #94a3b8; +} + +/* Table card wrapper */ +.table-card { + background: #fff; + border-radius: 12px; + border: 1px solid #e2e8f0; + overflow: hidden; + box-shadow: 0 1px 4px rgba(0,0,0,0.06); +} + +/* Sort controls row */ +.sort-controls { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 14px; + border-bottom: 1px solid #f1f5f9; +} +.sort-select { + border: 1px solid #e2e8f0; + border-radius: 8px; + background: #fff; + color: #0f172a; + font-size: 0.8125rem; + padding: 7px 28px 7px 10px; + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 8px center; + cursor: pointer; +} +.sort-select:focus { outline: none; border-color: #0a1d37; box-shadow: 0 0 0 3px rgba(10,29,55,0.08); } + +/* Primary button (navy) */ +.btn-primary { + display: inline-flex; + align-items: center; + gap: 6px; + background: #0a1d37; + color: #fff; + font-size: 0.875rem; + font-weight: 600; + padding: 9px 18px; + border-radius: 10px; + border: none; + cursor: pointer; + transition: background 150ms, box-shadow 150ms; + text-decoration: none; +} +.btn-primary:hover { background: #0f2a4e; box-shadow: 0 4px 12px rgba(10,29,55,0.25); } + +/* Secondary button */ +.btn-secondary { + display: inline-flex; + align-items: center; + gap: 6px; + background: #fff; + color: #0a1d37; + font-size: 0.875rem; + font-weight: 600; + padding: 9px 18px; + border-radius: 10px; + border: 1px solid #e2e8f0; + cursor: pointer; + transition: background 150ms, border-color 150ms; + text-decoration: none; +} +.btn-secondary:hover { background: #f8fafc; border-color: #cbd5e1; } + +/* Search input */ +.search-input { + width: 100%; + max-width: 320px; + border: 1px solid #e2e8f0; + border-radius: 8px; + background: #fff; + color: #0f172a; + font-size: 0.8125rem; + padding: 8px 12px; + transition: border-color 160ms, box-shadow 160ms; +} +.search-input::placeholder { color: #94a3b8; } +.search-input:focus { outline: none; border-color: rgba(10,29,55,0.45); box-shadow: 0 0 0 3px rgba(10,29,55,0.08); } + +/* Tab bar (orange underline style) */ +.tab-bar { display: flex; gap: 4px; border-bottom: 1px solid #e2e8f0; margin-bottom: 20px; } +.tab-link { + padding: 10px 16px; + font-size: 0.875rem; + font-weight: 600; + color: #64748b; + text-decoration: none; + border-bottom: 2px solid transparent; + margin-bottom: -1px; + transition: color 150ms, border-color 150ms; +} +.tab-link:hover { color: #0a1d37; } +.tab-link[aria-current='page'] { color: #fd6216; border-bottom-color: #fd6216; } + +/* Action icon buttons in tables */ +.action-btn { + display: inline-flex; + align-items: center; + justify-content: center; + height: 32px; + width: 32px; + border-radius: 7px; + border: 1px solid #e2e8f0; + background: #fff; + color: #475569; + cursor: pointer; + transition: background 120ms, color 120ms, border-color 120ms; +} +.action-btn:hover { background: #f8fafc; color: #0a1d37; border-color: #cbd5e1; } +.action-btn.danger:hover { background: #fff1f2; color: #e11d48; border-color: #fecdd3; } + +/* Status badge */ +.status-badge { + display: inline-flex; + align-items: center; + border-radius: 999px; + padding: 2px 10px; + font-size: 0.6875rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; +} +.status-active { background: #dcfce7; color: #166534; } +.status-pending { background: #fef9c3; color: #854d0e; } +.status-draft { background: #f1f5f9; color: #475569; } +.status-error { background: #fee2e2; color: #991b1b; } + /* ===== Admin Shell + Shared UI ===== */ .admin-shell-root { background: diff --git a/src/components/AdminShell.tsx b/src/components/AdminShell.tsx index 20c48da..6ca748b 100644 --- a/src/components/AdminShell.tsx +++ b/src/components/AdminShell.tsx @@ -3,6 +3,7 @@ import { For, createEffect, createMemo, createSignal, onCleanup, onMount, type J import AdminSidebar from './AdminSidebar'; import { isExternalIdentity } from '~/lib/admin-auth'; import { clearAdminSession, hasAdminSession, setAdminSession } from '~/lib/admin-session'; +import { Bell } from 'lucide-solid'; type Tab = { href: string; label: string; exact?: boolean }; @@ -25,43 +26,6 @@ const TAB_SETS: Array<{ prefixes: string[]; tabs: Tab[] }> = [ }, ]; -function IconBell() { - return ( - - - - - ); -} - -function IconSearch() { - return ( - - - - - ); -} - -function IconHelp() { - return ( - - - - - - ); -} - -function IconCog() { - return ( - - - - - ); -} - export default function AdminShell(props: { children: JSX.Element }) { const location = useLocation(); const navigate = useNavigate(); @@ -69,6 +33,7 @@ export default function AdminShell(props: { children: JSX.Element }) { const [checkedSession, setCheckedSession] = createSignal(false); const [adminName, setAdminName] = createSignal('Admin'); const [sidebarOpen, setSidebarOpen] = createSignal(false); + const [sidebarCollapsed, setSidebarCollapsed] = createSignal(false); const [tabsTrackEl, setTabsTrackEl] = createSignal(); const [tabRefs, setTabRefs] = createSignal>({}); const [tabIndicator, setTabIndicator] = createSignal({ left: 0, width: 0, ready: false }); @@ -81,7 +46,10 @@ export default function AdminShell(props: { children: JSX.Element }) { return []; }); - const isTabActive = (tab: Tab) => (tab.exact ? location.pathname === tab.href : location.pathname === tab.href || location.pathname.startsWith(`${tab.href}/`)); + const isTabActive = (tab: Tab) => + tab.exact + ? location.pathname === tab.href + : location.pathname === tab.href || location.pathname.startsWith(`${tab.href}/`); const refreshTabIndicator = () => { const activeTab = tabs().find((tab) => isTabActive(tab)); @@ -107,13 +75,16 @@ export default function AdminShell(props: { children: JSX.Element }) { onCleanup(() => window.removeEventListener('resize', onResize)); const isLocalDev = - typeof window !== 'undefined' && (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'); + typeof window !== 'undefined' && + (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'); const isPreview = searchParams._preview === '1' || - (typeof sessionStorage !== 'undefined' && sessionStorage.getItem('nxtgauge_admin_preview') === '1'); + (typeof sessionStorage !== 'undefined' && + sessionStorage.getItem('nxtgauge_admin_preview') === '1'); if (isPreview || isLocalDev) { - if (typeof sessionStorage !== 'undefined') sessionStorage.setItem('nxtgauge_admin_preview', '1'); + if (typeof sessionStorage !== 'undefined') + sessionStorage.setItem('nxtgauge_admin_preview', '1'); setAdminSession(); setCheckedSession(true); return; @@ -126,7 +97,10 @@ export default function AdminShell(props: { children: JSX.Element }) { return; } try { - const accessToken = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' : ''; + const accessToken = + typeof sessionStorage !== 'undefined' + ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' + : ''; const response = await fetch('/api/gateway/users/auth/me', { method: 'GET', headers: { @@ -166,73 +140,100 @@ export default function AdminShell(props: { children: JSX.Element }) { const adminInitials = createMemo(() => { const parts = adminName().split(' ').map((s) => s.trim()).filter(Boolean); - if (parts.length === 0) return 'AD'; - if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase(); - return `${parts[0][0] || ''}${parts[1][0] || ''}`.toUpperCase(); + if (parts.length === 0) return 'U'; + if (parts.length === 1) return parts[0].slice(0, 1).toUpperCase(); + return `${parts[0][0] || ''}`.toUpperCase(); }); + const sidebarWidth = () => (sidebarCollapsed() ? 'w-20' : 'w-64'); + return ( -
-
-
-
+
+ {/* ── Header ── */} +
+ {/* Left: logo + role title */} +
+ + NXTGAUGE + +

Super Admin

+
+ + {/* Mobile menu button */} + + + {/* Right: bell + avatar + logout */} +
+ {/* ── Body ── */} {checkedSession() ? ( -
+
+ {/* Mobile overlay */}
setSidebarOpen(false)} /> -
- setSidebarOpen(false)} onLogout={onLogout} /> + {/* Sidebar */} +
+ setSidebarCollapsed((v) => !v)} + onNavigate={() => setSidebarOpen(false)} + onLogout={onLogout} + />
-
+ {/* Main */} +
) : ( -
-