diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..d773874 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "admin-solid", + "runtimeExecutable": "sh", + "runtimeArgs": ["-c", "cd /Users/ashwin/workspace/nxtgauge-admin-solid && node .output/server/index.mjs"], + "port": 3000 + } + ] +} diff --git a/src/app.css b/src/app.css index 1ae6224..2a3d9f2 100644 --- a/src/app.css +++ b/src/app.css @@ -243,7 +243,13 @@ body { .shell { display: grid; grid-template-columns: 264px 1fr; - min-height: calc(100vh - 64px); + height: calc(100vh - 64px); + overflow: hidden; + transition: grid-template-columns 300ms ease; +} + +.shell.sidebar-collapsed { + grid-template-columns: 72px 1fr; } .sidebar { @@ -252,7 +258,8 @@ body { padding: 20px 12px 12px; display: flex; flex-direction: column; - min-height: 0; + height: 100%; + overflow: hidden; } .sidebar-toggle-row { @@ -278,6 +285,43 @@ body { color: #334155; } +.sidebar-chevron { + display: inline-block; + transition: transform 300ms ease; +} + +.sidebar-chevron.collapsed { + transform: rotate(180deg); +} + +/* Collapsed sidebar */ +.sidebar.sidebar-collapsed { + padding: 20px 6px 12px; + align-items: center; +} + +.sidebar.sidebar-collapsed .sidebar-toggle-row { + padding: 0; + justify-content: center; +} + +.sidebar.sidebar-collapsed .nav-item { + justify-content: center; + padding: 10px; + gap: 0; +} + +.collapsed-dot { + position: absolute; + right: -2px; + top: 50%; + transform: translateY(-50%); + width: 8px; + height: 8px; + border-radius: 999px; + background: var(--brand-orange); +} + .sidebar-nav { flex: 1; min-height: 0; @@ -293,13 +337,15 @@ body { text-decoration: none; color: #475569; border: 1px solid transparent; - border-radius: 12px; - padding: 11px 12px; - margin-bottom: 6px; - font-size: 15px; + border-radius: 10px; + padding: 10px 12px; + margin-bottom: 2px; + font-size: 13.5px; line-height: 1.35; font-weight: 500; - transition: all 180ms ease; + transition: background 180ms ease, border-color 180ms ease, color 180ms ease, box-shadow 180ms ease; + /* Prevent layout shift: active state changes font-weight which could reflow text */ + min-height: 40px; } .nav-dot { @@ -337,6 +383,17 @@ body { .nav-title { flex: 1; + /* Reserve space for bold state so text reflow doesn't shift layout */ + display: grid; +} +.nav-title::after { + content: attr(data-text); + height: 0; + overflow: hidden; + visibility: hidden; + font-weight: 700; + pointer-events: none; + user-select: none; } .active-badge { @@ -353,18 +410,20 @@ body { .main { min-width: 0; - padding: 14px 16px 16px; + overflow-y: auto; + height: 100%; } .main-inner { - max-width: 1180px; + max-width: 1200px; + padding: 20px 24px 32px; } .admin-tab-wrap { border-bottom: 1px solid #e2e8f0; background: #fff; - margin: -2px -16px 16px; - padding: 0 16px; + margin: -20px -24px 20px; + padding: 0 24px; } .admin-tabs { @@ -375,12 +434,16 @@ body { .admin-tab { text-decoration: none; + border: none; border-bottom: 2px solid transparent; + background: transparent; padding: 12px 0; font-size: 14px; font-weight: 600; color: #475569; margin-bottom: -1px; + cursor: pointer; + font-family: 'Exo 2', sans-serif; transition: color 140ms ease, border-color 140ms ease; } @@ -397,13 +460,14 @@ body { /* ---- Shared Content ---- */ .page-title { margin: 0; - font-size: 30px; + font-size: 22px; font-weight: 800; color: #0f172a; } .page-subtitle { - margin-top: 8px; + margin: 4px 0 0; + font-size: 13px; color: #64748b; } @@ -670,3 +734,692 @@ body { height: 36px; } } + +/* ---- Builder Components ---- */ +.builder-header { + background: #fff1e8; + border: 1px solid #ffc9ac; + border-radius: 16px; + padding: 16px; + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + margin-bottom: 16px; +} + +.builder-header h2 { + margin: 0; + font-size: 18px; + font-weight: 700; + color: #111827; +} + +.builder-header p { + margin: 4px 0 0; + font-size: 13px; + color: #475569; +} + +.builder-header-actions { + display: flex; + gap: 8px; + flex-shrink: 0; +} + +.builder-tab-bar { + display: flex; + flex-wrap: wrap; + gap: 8px; + background: #fff; + border: 1px solid #e2e8f0; + border-radius: 12px; + padding: 8px; + margin-bottom: 16px; +} + +.builder-tab-btn { + border: 1px solid transparent; + background: transparent; + border-radius: 8px; + padding: 8px 16px; + font-size: 14px; + font-weight: 500; + color: #475569; + cursor: pointer; + transition: all 150ms ease; + font-family: 'Exo 2', sans-serif; +} + +.builder-tab-btn:hover { + background: #f8fafc; +} + +.builder-tab-btn.active { + border-color: #ffc9ac; + background: #fff1e8; + color: #c2410c; +} + +.builder-section { + border: 1px solid #e2e8f0; + border-radius: 16px; + padding: 16px; + margin-bottom: 12px; +} + +.builder-section-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 12px; +} + +.builder-section-header input { + flex: 1; + border: 1px solid #cbd5e1; + border-radius: 12px; + padding: 8px 12px; + font-size: 14px; + font-weight: 600; + outline: none; + font-family: 'Exo 2', sans-serif; +} + +.builder-section-header input:focus { + border-color: var(--brand-orange); + box-shadow: 0 0 0 3px rgba(253, 98, 22, 0.14); +} + +.builder-item { + display: grid; + gap: 8px; + border: 1px solid #e2e8f0; + border-radius: 12px; + padding: 12px; + margin-bottom: 8px; + background: #fff; +} + +.builder-item input, +.builder-item textarea, +.builder-item select { + border: 1px solid #cbd5e1; + border-radius: 12px; + padding: 8px 12px; + font-size: 14px; + outline: none; + background: #fff; + font-family: 'Exo 2', sans-serif; +} + +.builder-item input:focus, +.builder-item textarea:focus, +.builder-item select:focus { + border-color: var(--brand-orange); + box-shadow: 0 0 0 3px rgba(253, 98, 22, 0.14); +} + +.builder-item-row-4 { + grid-template-columns: 1fr 90px 90px 110px; +} + +.btn.danger { + border-color: #fca5a5; + background: #fff; + color: #b91c1c; +} + +.btn.danger:hover { + border-color: #ef4444; + background: #fef2f2; +} + +.btn.orange { + border-color: #ffc9ac; + background: #fff1e8; + color: #c2410c; + font-size: 12px; + padding: 6px 12px; +} + +.btn.orange:hover { + background: #ffe2d2; +} + +.btn.navy { + border-color: #050026; + background: #050026; + color: #fff; +} + +.btn.navy:hover { + background: #0a0040; +} + +.field-grid-2 { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; +} + +.sub-card { + border: 1px solid #e2e8f0; + border-radius: 12px; + background: #f8fafc; + padding: 16px; + margin-top: 12px; +} + +.sub-card-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; +} + +.sub-card-header h4 { + margin: 0; + font-size: 14px; + font-weight: 700; + color: #111827; +} + +.nested-card { + border: 1px solid #e2e8f0; + border-radius: 12px; + background: #fff; + padding: 16px; + margin-bottom: 8px; +} + +.nested-card-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 12px; +} + +.nested-card-header input { + flex: 1; + border: 1px solid #cbd5e1; + border-radius: 12px; + padding: 8px 12px; + font-size: 14px; + font-weight: 600; + outline: none; + font-family: 'Exo 2', sans-serif; +} + +.nested-card-header input:focus { + border-color: var(--brand-orange); + box-shadow: 0 0 0 3px rgba(253, 98, 22, 0.14); +} + +.widget-item { + border: 1px solid #e2e8f0; + border-radius: 12px; + background: #fff; + padding: 12px; + margin-bottom: 8px; +} + +.widget-item input, +.widget-item textarea { + width: 100%; + border: 1px solid #cbd5e1; + border-radius: 8px; + padding: 8px 12px; + font-size: 13px; + outline: none; + margin-bottom: 6px; + font-family: 'Exo 2', sans-serif; +} + +.widget-item input:focus, +.widget-item textarea:focus { + border-color: var(--brand-orange); + box-shadow: 0 0 0 3px rgba(253, 98, 22, 0.14); +} + +/* Permission table for internal role management */ +.perm-table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + font-size: 13px; + overflow: hidden; + border-radius: 12px; + border: 1px solid #e2e8f0; +} + +.perm-table thead th { + background: #0B0720; + color: #fff; + padding: 12px 16px; + text-align: left; + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.perm-table thead th:not(:first-child) { + text-align: center; +} + +.perm-table tbody td { + border-bottom: 1px solid #e2e8f0; + padding: 12px 16px; + color: #334155; + background: #fff; +} + +.perm-table tbody td:not(:first-child) { + text-align: center; +} + +.perm-table tbody tr:hover td { + background: #fff7ed; +} + +.module-picker { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 8px; + margin-top: 12px; +} + +.module-chip { + display: flex; + align-items: center; + gap: 8px; + border: 1px solid #e2e8f0; + border-radius: 8px; + padding: 8px 12px; + cursor: pointer; + font-size: 13px; + background: #fff; + transition: all 150ms ease; + font-family: 'Exo 2', sans-serif; + text-align: left; +} + +.module-chip:hover { + border-color: #ffc9ac; + background: #fff1e8; +} + +.module-chip.selected { + border-color: #ffc9ac; + background: #fff1e8; + color: #c2410c; + font-weight: 600; +} + +/* Builder preview */ +.preview-shell { + border: 1px solid #e2e8f0; + border-radius: 16px; + overflow: hidden; + background: #f1f5f9; +} + +.preview-header { + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid #e2e8f0; + background: #fff; + padding: 16px 20px; +} + +.preview-layout { + display: grid; + grid-template-columns: 260px 1fr; + min-height: 500px; +} + +.preview-sidebar { + border-right: 1px solid #e2e8f0; + background: #fcfcfd; + padding: 12px; +} + +.preview-sidebar-item { + display: flex; + align-items: center; + gap: 10px; + border-radius: 12px; + padding: 10px 12px; + margin-bottom: 4px; + cursor: pointer; + font-size: 14px; + color: #475569; + text-align: left; + border: 1px solid transparent; + background: transparent; + width: 100%; + transition: all 150ms ease; + font-family: 'Exo 2', sans-serif; +} + +.preview-sidebar-item:hover { + background: #fff; + color: #0f172a; +} + +.preview-sidebar-item.active { + background: #fff1e8; + color: #111827; + font-weight: 600; + box-shadow: inset 3px 0 0 0 #fd6216; +} + +.preview-content { + padding: 24px; + overflow-y: auto; +} + +.preview-section { + border: 1px solid #e2e8f0; + border-radius: 16px; + background: #fff; + padding: 20px; + margin-bottom: 16px; +} + +.preview-tabs { + display: flex; + gap: 8px; + border-bottom: 1px solid #e2e8f0; + padding-bottom: 12px; + margin-top: 16px; + flex-wrap: wrap; +} + +.preview-tab-btn { + border-radius: 8px; + padding: 8px 16px; + font-size: 13px; + font-weight: 500; + cursor: pointer; + border: 1px solid transparent; + background: transparent; + color: #475569; + font-family: 'Exo 2', sans-serif; +} + +.preview-tab-btn.active { + border-color: #ffc9ac; + background: #fff1e8; + color: #c2410c; +} + +.preview-fields-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; + margin-top: 20px; +} + +.preview-field label { + display: block; + font-size: 13px; + font-weight: 600; + color: #334155; + margin-bottom: 6px; +} + +.preview-field input, +.preview-field select { + width: 100%; + border: 1px solid #cbd5e1; + border-radius: 12px; + padding: 10px 12px; + font-size: 13px; + background: #fff; + outline: none; +} + +.preview-widget-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12px; + margin-top: 20px; +} + +.preview-widget { + border: 1px solid #e2e8f0; + border-radius: 12px; + background: #f8fafc; + padding: 16px; +} + +.preview-widget .w-label { + font-size: 12px; + color: #64748b; +} + +.preview-widget .w-metric { + font-size: 22px; + font-weight: 700; + color: #050026; + margin-top: 8px; +} + +.preview-widget .w-desc { + font-size: 11px; + color: #64748b; + margin-top: 4px; +} + +/* Onboarding step builder */ +.step-builder { + border: 1px solid #e2e8f0; + border-radius: 16px; + background: #fff; + padding: 16px; + margin-bottom: 12px; +} + +.step-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 12px; +} + +.step-num { + background: #050026; + color: #fff; + border-radius: 999px; + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + font-size: 13px; + font-weight: 700; + flex-shrink: 0; +} + +.step-title-input { + flex: 1; + border: 1px solid #cbd5e1; + border-radius: 12px; + padding: 8px 12px; + font-size: 14px; + font-weight: 600; + outline: none; + font-family: 'Exo 2', sans-serif; +} + +.step-title-input:focus { + border-color: var(--brand-orange); + box-shadow: 0 0 0 3px rgba(253, 98, 22, 0.14); +} + +.field-type-select { + border: 1px solid #cbd5e1; + border-radius: 8px; + padding: 6px 10px; + font-size: 12px; + background: #fff; + outline: none; + font-family: 'Exo 2', sans-serif; +} + +.field-row { + display: grid; + grid-template-columns: 1fr 130px 100px auto; + gap: 8px; + align-items: center; + border: 1px solid #e2e8f0; + border-radius: 10px; + padding: 10px 12px; + margin-bottom: 6px; + background: #fff; +} + +.field-row input, +.field-row select { + border: 1px solid #cbd5e1; + border-radius: 8px; + padding: 6px 10px; + font-size: 13px; + outline: none; + font-family: 'Exo 2', sans-serif; +} + +.field-row input:focus, +.field-row select:focus { + border-color: var(--brand-orange); +} + +.error-box { + background: #fef2f2; + border: 1px solid #fca5a5; + border-radius: 12px; + padding: 12px 16px; + color: #b91c1c; + font-size: 13px; + margin-bottom: 12px; +} + +.success-note { + margin-top: 8px; + font-size: 12px; + color: #047857; + font-weight: 700; +} + +.info-box { + background: #fff; + border: 1px solid #e2e8f0; + border-radius: 12px; + padding: 12px 16px; + font-size: 13px; + color: #475569; +} + +.role-detail-card { + background: #fff; + border: 1px solid #e2e8f0; + border-radius: 12px; + padding: 24px; + margin-bottom: 16px; +} + +.role-field-readonly { + width: 100%; + border: 1px solid #e2e8f0; + border-radius: 8px; + padding: 10px 12px; + font-size: 14px; + background: #f8fafc; + color: #64748b; + cursor: not-allowed; +} + +.page-actions { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 20px; +} + +.page-actions-right { + display: flex; + gap: 8px; +} + +.role-form-section { + background: #fff; + border: 1px solid #e2e8f0; + border-radius: 16px; + padding: 24px; + margin-bottom: 16px; +} + +.role-form-section h3 { + margin: 0 0 4px; + font-size: 13px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + color: #64748b; +} + +.role-form-section p { + margin: 0 0 16px; + font-size: 13px; + color: #64748b; +} + +.external-role-form .field select { + width: 100%; +} + +.checkbox-label { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + cursor: pointer; +} + +.checkbox-label input[type="checkbox"] { + width: 16px; + height: 16px; +} + +.onboarding-info-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12px; + margin-bottom: 16px; +} + +.onboarding-stat { + border: 1px solid #e2e8f0; + border-radius: 12px; + background: #fff; + padding: 14px 16px; +} + +.onboarding-stat .stat-label { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.1em; + color: #64748b; +} + +.onboarding-stat .stat-value { + font-size: 18px; + font-weight: 700; + color: #050026; + margin-top: 4px; +} diff --git a/src/components/AdminShell.tsx b/src/components/AdminShell.tsx index bfd9cc7..d1d2f42 100644 --- a/src/components/AdminShell.tsx +++ b/src/components/AdminShell.tsx @@ -1,24 +1,72 @@ import { A, useLocation, useNavigate } from '@solidjs/router'; -import { createSignal, onMount, type JSX } from 'solid-js'; +import { createMemo, createSignal, onMount, type JSX } from 'solid-js'; import AdminSidebar from './AdminSidebar'; import { clearAdminSession, hasAdminSession } from '~/lib/admin-session'; +import { sidebarCollapsed } from '~/lib/sidebar-state'; + +type Tab = { href: string; label: string; exact?: boolean }; + +const TAB_SETS: Array<{ prefixes: string[]; tabs: Tab[] }> = [ + { + prefixes: ['/admin/roles'], + tabs: [ + { href: '/admin/roles', label: 'Internal Roles', exact: true }, + { href: '/admin/roles/create', label: 'Create Role' }, + ], + }, + { + prefixes: ['/admin/runtime-roles'], + tabs: [ + { href: '/admin/runtime-roles', label: 'External Roles', exact: true }, + { href: '/admin/runtime-roles/new', label: 'Create External Role' }, + ], + }, + { + prefixes: ['/admin/onboarding-schemas'], + tabs: [ + { href: '/admin/onboarding-schemas', label: 'Onboarding Flows', exact: true }, + { href: '/admin/onboarding-schemas/new', label: 'Create Flow' }, + ], + }, + { + prefixes: ['/admin/internal-dashboard-management'], + tabs: [ + { href: '/admin/internal-dashboard-management', label: 'Internal Dashboards' }, + ], + }, + { + prefixes: ['/admin/external-dashboard-management'], + tabs: [ + { href: '/admin/external-dashboard-management', label: 'External Dashboards' }, + ], + }, + { + prefixes: ['/admin/role-ui-configs'], + tabs: [ + { href: '/admin/role-ui-configs', label: 'Config Inspector', exact: true }, + { href: '/admin/role-ui-configs/new', label: 'Create Config' }, + ], + }, +]; export default function AdminShell(props: { children: JSX.Element }) { const location = useLocation(); const navigate = useNavigate(); const [checkedSession, setCheckedSession] = createSignal(false); - const tabs = [ - { href: '/admin/runtime-roles', label: 'View Roles' }, - { href: '/admin/runtime-roles/new', label: 'Create Role' }, - { href: '/admin/role-ui-configs', label: 'Inspector' }, - { href: '/admin/onboarding-schemas', label: 'Onboarding' }, - ]; - const isTabActive = (href: string) => { - if (href === '/admin/runtime-roles') { - return location.pathname === href || (location.pathname.startsWith('/admin/runtime-roles/') && location.pathname !== '/admin/runtime-roles/new'); + const tabs = createMemo(() => { + const path = location.pathname; + for (const set of TAB_SETS) { + if (set.prefixes.some((p) => path === p || path.startsWith(`${p}/`))) { + return set.tabs; + } } - return location.pathname === href || location.pathname.startsWith(`${href}/`); + return []; + }); + + const isTabActive = (tab: Tab) => { + if (tab.exact) return location.pathname === tab.href; + return location.pathname === tab.href || location.pathname.startsWith(`${tab.href}/`); }; onMount(() => { @@ -54,18 +102,20 @@ export default function AdminShell(props: { children: JSX.Element }) { {checkedSession() ? ( -
+
-
- -
+ {tabs().length > 0 ? ( +
+ +
+ ) : null}
{props.children}
diff --git a/src/components/AdminSidebar.tsx b/src/components/AdminSidebar.tsx index 4c11089..b1a1c3f 100644 --- a/src/components/AdminSidebar.tsx +++ b/src/components/AdminSidebar.tsx @@ -1,4 +1,5 @@ import { A, useLocation } from '@solidjs/router'; +import { sidebarCollapsed, toggleSidebar } from '~/lib/sidebar-state'; type LinkItem = { legacyHref: string; href: string; label: string; icon: string; aliasPrefix?: string }; @@ -7,9 +8,9 @@ const links: LinkItem[] = [ { legacyHref: '/department', href: '/admin/department', label: 'Department Management', icon: 'department.svg' }, { legacyHref: '/designation', href: '/admin/designation', label: 'Designation Management', icon: 'designation.svg' }, { legacyHref: '/employees', href: '/admin/employees', label: 'Internal User Management', icon: 'users.svg' }, - { legacyHref: '/roles?scope=internal', href: '/admin/roles?scope=internal', label: 'Internal Role Management', icon: 'role.svg' }, + { legacyHref: '/roles?scope=internal', href: '/admin/roles', label: 'Internal Role Management', icon: 'role.svg' }, { legacyHref: '/runtime-roles', href: '/admin/runtime-roles', label: 'External Role Management', icon: 'role.svg' }, - { legacyHref: '/onboarding-management', href: '/admin/onboarding-management', label: 'Onboarding Management', icon: 'reviews.svg', aliasPrefix: '/admin/onboarding-schemas' }, + { legacyHref: '/onboarding-management', href: '/admin/onboarding-schemas', label: 'Onboarding Management', icon: 'reviews.svg' }, { legacyHref: '/internal-dashboard-management', href: '/admin/internal-dashboard-management', label: 'Internal Dashboard Management', icon: 'dashboard.svg' }, { legacyHref: '/external-dashboard-management', href: '/admin/external-dashboard-management', label: 'External Dashboard Management', icon: 'dashboard.svg', aliasPrefix: '/admin/role-ui-configs' }, { legacyHref: '/approval', href: '/admin/approval', label: 'Approval Management', icon: 'approval.svg' }, @@ -45,6 +46,8 @@ const links: LinkItem[] = [ export default function AdminSidebar() { const location = useLocation(); + const collapsed = sidebarCollapsed; + const isLinkActive = (href: string, aliasPrefix?: string) => { const pathOnly = href.split('?')[0] || href; if (pathOnly === '/admin') return location.pathname === '/admin'; @@ -53,18 +56,42 @@ export default function AdminSidebar() { }; return ( -