MAC / frontend /src /lib /components /Sidebar.svelte
Aaryan17's picture
chore: upload MAC codebase to HF Space
0e76632 verified
<script>
import { onMount, onDestroy } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { authStore, isAdmin, isFacultyOrAdmin, showToast } from '$lib/stores.js';
import { t } from '$lib/i18n.js';
const MIN_W = 52;
const MAX_W = 360;
const DEFAULT_W = 220;
const COMPACT_THRESHOLD = 72;
let sidebarWidth = DEFAULT_W;
let compact = false;
let resizerEl;
let dragging = false;
let dragStartX = 0;
let dragStartW = 0;
$: path = $page.url.pathname;
$: compact = sidebarWidth < COMPACT_THRESHOLD;
const navItems = [
{ href: '/chat', key: 'nav.chat', label: 'Chat' },
{ href: '/dashboard', key: 'nav.dashboard', label: 'Dashboard' },
{ href: '/notebooks', key: 'nav.notebooks', label: 'Notebooks' },
{ href: '/rag', key: 'nav.rag', label: 'Knowledge' },
{ href: '/doubts', key: 'nav.doubts', label: 'Doubts' },
{ href: '/attendance', key: 'nav.attendance', label: 'Attendance' },
{ href: '/files', key: 'nav.files', label: 'Files' },
{ href: '/notifications', key: 'nav.notifications', label: 'Notifications' },
{ href: '/keys', key: 'nav.keys', label: 'API Keys' },
{ href: '/settings', key: 'nav.settings', label: 'Settings' },
];
const adminItems = [
{ href: '/copy-check', key: 'nav.copycheck', label: 'Copy Check', faculty: true },
{ href: '/admin', key: 'nav.admin', label: 'Admin' },
{ href: '/cluster', key: 'nav.cluster', label: 'Cluster' },
];
// ── SVG icons ────────────────────────────────────────────────────────────────
const icons = {
chat: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>`,
dashboard: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/></svg>`,
notebooks: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5V5a2 2 0 0 1 2-2h11a3 3 0 0 1 3 3v13.5"/><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M8 7h7"/><path d="M8 11h5"/></svg>`,
rag: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>`,
doubts: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 18h.01"/><path d="M9.1 9a3 3 0 1 1 5.2 2c-.9.8-1.8 1.3-2.1 2.7"/><path d="M21 12a9 9 0 1 1-4.2-7.6"/></svg>`,
attendance: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-8 0v2"/><circle cx="12" cy="7" r="4"/><path d="m17 11 2 2 4-4"/></svg>`,
files: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg>`,
notifications: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>`,
keys: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="7.5" cy="15.5" r="5.5"/><path d="M21 2l-9.6 9.6"/><path d="M15.5 7.5l3 3L22 7l-3-3"/></svg>`,
settings: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>`,
copycheck: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>`,
admin: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L3 7l1.5 10L12 22l7.5-5L21 7z"/><circle cx="12" cy="12" r="3"/></svg>`,
cluster: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg>`,
logout: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>`,
logo: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>`,
};
const iconMap = {
'/chat': 'chat',
'/dashboard': 'dashboard',
'/notebooks': 'notebooks',
'/rag': 'rag',
'/doubts': 'doubts',
'/attendance': 'attendance',
'/files': 'files',
'/notifications': 'notifications',
'/keys': 'keys',
'/settings': 'settings',
'/copy-check': 'copycheck',
'/admin': 'admin',
'/cluster': 'cluster',
};
// ── Resize logic ─────────────────────────────────────────────────────────────
function onMouseDown(e) {
dragging = true;
dragStartX = e.clientX;
dragStartW = sidebarWidth;
e.preventDefault();
}
function onMouseMove(e) {
if (!dragging) return;
const delta = e.clientX - dragStartX;
sidebarWidth = Math.min(MAX_W, Math.max(MIN_W, dragStartW + delta));
}
function onMouseUp() {
if (dragging) {
dragging = false;
try { localStorage.setItem('mac_sidebar_w', String(sidebarWidth)); } catch {}
}
}
function onDblClick() {
sidebarWidth = compact ? DEFAULT_W : MIN_W;
try { localStorage.setItem('mac_sidebar_w', String(sidebarWidth)); } catch {}
}
// ── Touch resize ─────────────────────────────────────────────────────────────
function onTouchStart(e) {
dragging = true;
dragStartX = e.touches[0].clientX;
dragStartW = sidebarWidth;
}
function onTouchMove(e) {
if (!dragging) return;
const delta = e.touches[0].clientX - dragStartX;
sidebarWidth = Math.min(MAX_W, Math.max(MIN_W, dragStartW + delta));
e.preventDefault();
}
function onTouchEnd() {
if (dragging) {
dragging = false;
try { localStorage.setItem('mac_sidebar_w', String(sidebarWidth)); } catch {}
}
}
async function logout() {
await authStore.logout();
goto('/login');
}
onMount(() => {
try {
const saved = localStorage.getItem('mac_sidebar_w');
if (saved) sidebarWidth = Math.min(MAX_W, Math.max(MIN_W, Number(saved)));
} catch {}
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
});
onDestroy(() => {
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
});
</script>
<aside
class="sidebar"
class:compact
class:dragging
style="width: {sidebarWidth}px; min-width: {sidebarWidth}px;"
>
<!-- Logo -->
<div class="logo-area">
<span class="glitch sidebar-mac" data-text="MAC">MAC</span>
{#if !compact}
<span class="logo-sub">MBM AI Cloud</span>
{/if}
</div>
<!-- Nav -->
<nav class="nav-section">
{#each navItems as item}
<a
href={item.href}
class="nav-link"
class:active={path.startsWith(item.href)}
title={compact ? ($t(item.key) || item.label) : ''}
>
<span class="nav-icon">{@html icons[iconMap[item.href]]}</span>
{#if !compact}
<span class="nav-label">{$t(item.key) || item.label}</span>
{/if}
</a>
{/each}
{#if $isFacultyOrAdmin}
{#if !compact}
<div class="section-header">Controls</div>
{:else}
<div class="section-divider"></div>
{/if}
{#each adminItems as item}
{#if item.faculty || $isAdmin}
<a
href={item.href}
class="nav-link"
class:active={path.startsWith(item.href)}
title={compact ? ($t(item.key) || item.label) : ''}
>
<span class="nav-icon">{@html icons[iconMap[item.href]]}</span>
{#if !compact}
<span class="nav-label">{$t(item.key) || item.label}</span>
{/if}
</a>
{/if}
{/each}
{/if}
</nav>
<!-- User + Logout -->
<div class="user-area">
<div class="user-info" class:compact-user={compact}>
<div class="avatar">
{$authStore.user?.name?.charAt(0)?.toUpperCase() ?? '?'}
</div>
{#if !compact}
<div class="user-text">
<span class="user-name">{$authStore.user?.name ?? ''}</span>
<span class="user-role">{$authStore.user?.role ?? ''}</span>
</div>
{/if}
</div>
<button
class="nav-link logout-btn"
on:click={logout}
title={compact ? 'Logout' : ''}
>
<span class="nav-icon">{@html icons.logout}</span>
{#if !compact}
<span class="nav-label">Logout</span>
{/if}
</button>
</div>
<!-- Resize handle -->
<div
class="resizer"
bind:this={resizerEl}
on:mousedown={onMouseDown}
on:dblclick={onDblClick}
on:touchstart|passive={onTouchStart}
on:touchmove|preventDefault={onTouchMove}
on:touchend={onTouchEnd}
role="separator"
aria-label="Resize sidebar"
>
<div class="resizer-line"></div>
</div>
</aside>
<style>
.sidebar {
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
height: 100vh;
background: var(--surface);
border-right: 1px solid var(--border);
flex-shrink: 0;
transition: width 0.15s ease;
overflow: hidden;
user-select: none;
}
.sidebar.dragging {
transition: none;
}
/* ── Logo ─────────────────────────────────────── */
.logo-area {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 14px 14px 14px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
overflow: hidden;
}
/* Sidebar MAC text β€” uses global .glitch class, just size it here */
.sidebar-mac {
font-size: 16px;
flex-shrink: 0;
}
.logo-sub {
font-size: 10px;
color: var(--text3);
white-space: nowrap;
margin-top: 1px;
}
/* ── Nav ──────────────────────────────────────── */
.nav-section {
flex: 1;
padding: 8px 6px;
overflow-y: auto;
overflow-x: hidden;
display: flex;
flex-direction: column;
gap: 2px;
}
.nav-section::-webkit-scrollbar { width: 4px; }
.nav-section::-webkit-scrollbar-track { background: transparent; }
.nav-section::-webkit-scrollbar-thumb { background: var(--surface3); border-radius: 2px; }
.nav-link {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
border-radius: 8px;
color: var(--text2);
font-size: 13px;
font-weight: 500;
text-decoration: none;
cursor: pointer;
background: none;
border: none;
width: 100%;
transition: background 0.15s, color 0.15s;
white-space: nowrap;
overflow: hidden;
}
.nav-link:hover {
background: var(--surface2);
color: var(--text);
}
.nav-link.active {
background: rgba(217,116,73,0.10);
color: var(--accent);
}
.nav-link.active .nav-icon :global(svg) {
stroke: var(--accent);
}
.nav-icon {
width: 18px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.nav-icon :global(svg) {
width: 18px;
height: 18px;
stroke: currentColor;
flex-shrink: 0;
}
.nav-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
.section-header {
padding: 10px 12px 4px;
font-size: 10px;
color: var(--text3);
text-transform: uppercase;
letter-spacing: 0.12em;
font-weight: 600;
}
.section-divider {
height: 1px;
background: var(--border);
margin: 6px 8px;
}
/* ── Compact mode ─────────────────────────────── */
.compact .nav-link {
justify-content: center;
padding: 10px;
gap: 0;
}
.compact .logo-area {
justify-content: center;
padding: 14px 10px;
}
/* ── User area ────────────────────────────────── */
.user-area {
border-top: 1px solid var(--border);
padding: 8px 6px;
flex-shrink: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.user-info {
display: flex;
align-items: center;
gap: 10px;
padding: 6px 10px 4px;
overflow: hidden;
}
.user-info.compact-user {
justify-content: center;
padding: 6px 10px;
}
.avatar {
width: 28px;
height: 28px;
border-radius: 50%;
background: rgba(217,116,73,0.15);
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 700;
color: var(--accent);
flex-shrink: 0;
}
.user-text {
display: flex;
flex-direction: column;
min-width: 0;
overflow: hidden;
}
.user-name {
font-size: 12px;
font-weight: 600;
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.user-role {
font-size: 10px;
color: var(--text3);
text-transform: capitalize;
white-space: nowrap;
}
.logout-btn {
color: var(--text3);
}
.logout-btn:hover {
background: var(--error-bg);
color: var(--error);
}
/* ── Resize handle ────────────────────────────── */
.resizer {
position: absolute;
top: 0;
right: 0;
width: 8px;
height: 100%;
cursor: col-resize;
z-index: 10;
display: flex;
align-items: stretch;
justify-content: flex-end;
}
.resizer-line {
width: 2px;
background: transparent;
transition: background 0.2s;
border-radius: 1px;
margin-right: 1px;
}
.resizer:hover .resizer-line,
.dragging .resizer .resizer-line {
background: var(--accent);
}
</style>