feat(web): design system — ms-* tokens, ThemeProvider, MosaicLogo, sidebar (#221)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #221.
This commit is contained in:
@@ -3,58 +3,178 @@
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { cn } from '@/lib/cn';
|
||||
import { MosaicLogo } from '@/components/ui/mosaic-logo';
|
||||
import { useSidebar } from './sidebar-context';
|
||||
|
||||
interface NavItem {
|
||||
label: string;
|
||||
href: string;
|
||||
icon: string;
|
||||
icon: React.JSX.Element;
|
||||
}
|
||||
|
||||
function IconChat(): React.JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
>
|
||||
<path d="M3 4.5A2.5 2.5 0 0 1 5.5 2h5A2.5 2.5 0 0 1 13 4.5v3A2.5 2.5 0 0 1 10.5 10H8l-3.5 3v-3H5.5A2.5 2.5 0 0 1 3 7.5z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function IconTasks(): React.JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
>
|
||||
<path d="M6 3h7M6 8h7M6 13h7" />
|
||||
<path d="M2.5 3.5 3.5 4.5 5 2.5M2.5 8.5 3.5 9.5 5 7.5M2.5 13.5 3.5 14.5 5 12.5" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function IconProjects(): React.JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
>
|
||||
<path d="M2 4.5A1.5 1.5 0 0 1 3.5 3h3l1.5 1.5h4A1.5 1.5 0 0 1 13.5 6v5.5A1.5 1.5 0 0 1 12 13H3.5A1.5 1.5 0 0 1 2 11.5z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function IconSettings(): React.JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
>
|
||||
<circle cx="8" cy="8" r="2.25" />
|
||||
<path d="M8 1.5v2M8 12.5v2M1.5 8h2M12.5 8h2M3.05 3.05l1.4 1.4M11.55 11.55l1.4 1.4M3.05 12.95l1.4-1.4M11.55 4.45l1.4-1.4" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function IconAdmin(): React.JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
>
|
||||
<path d="M8 1.75 13 3.5v3.58c0 3.12-1.88 5.94-5 7.17-3.12-1.23-5-4.05-5-7.17V3.5z" />
|
||||
<path d="M6.25 7.75 7.5 9l2.5-2.5" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const navItems: NavItem[] = [
|
||||
{ label: 'Chat', href: '/chat', icon: '💬' },
|
||||
{ label: 'Tasks', href: '/tasks', icon: '📋' },
|
||||
{ label: 'Projects', href: '/projects', icon: '📁' },
|
||||
{ label: 'Settings', href: '/settings', icon: '⚙️' },
|
||||
{ label: 'Admin', href: '/admin', icon: '🛡️' },
|
||||
{ label: 'Chat', href: '/chat', icon: <IconChat /> },
|
||||
{ label: 'Tasks', href: '/tasks', icon: <IconTasks /> },
|
||||
{ label: 'Projects', href: '/projects', icon: <IconProjects /> },
|
||||
{ label: 'Settings', href: '/settings', icon: <IconSettings /> },
|
||||
{ label: 'Admin', href: '/admin', icon: <IconAdmin /> },
|
||||
];
|
||||
|
||||
export function Sidebar(): React.ReactElement {
|
||||
const pathname = usePathname();
|
||||
const { mobileOpen, setMobileOpen } = useSidebar();
|
||||
|
||||
return (
|
||||
<aside className="fixed left-0 top-0 z-30 flex h-screen w-sidebar flex-col border-r border-surface-border bg-surface-card">
|
||||
<div className="flex h-14 items-center px-4">
|
||||
<Link href="/" className="text-lg font-semibold text-text-primary">
|
||||
Mosaic
|
||||
</Link>
|
||||
</div>
|
||||
<>
|
||||
<aside
|
||||
className="app-sidebar"
|
||||
data-mobile-open={mobileOpen ? 'true' : undefined}
|
||||
style={{
|
||||
width: 'var(--sidebar-w)',
|
||||
background: 'var(--surface)',
|
||||
borderRightColor: 'var(--border)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="flex h-16 items-center gap-3 border-b px-5"
|
||||
style={{ borderColor: 'var(--border)' }}
|
||||
>
|
||||
<MosaicLogo size={32} />
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<span className="text-sm font-semibold uppercase tracking-[0.12em] text-[var(--text)]">
|
||||
Mosaic
|
||||
</span>
|
||||
<span className="text-xs text-[var(--muted)]">Mission Control</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 space-y-1 px-2 py-2">
|
||||
{navItems.map((item) => {
|
||||
const isActive = pathname === item.href || pathname.startsWith(`${item.href}/`);
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
'flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors',
|
||||
isActive
|
||||
? 'bg-blue-600/20 text-blue-400'
|
||||
: 'text-text-secondary hover:bg-surface-elevated hover:text-text-primary',
|
||||
)}
|
||||
>
|
||||
<span className="text-base" aria-hidden="true">
|
||||
{item.icon}
|
||||
</span>
|
||||
{item.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
<nav className="flex-1 px-3 py-4">
|
||||
<div className="mb-3 px-2 text-[11px] font-medium uppercase tracking-[0.18em] text-[var(--muted)]">
|
||||
Workspace
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
{navItems.map((item) => {
|
||||
const isActive = pathname === item.href || pathname.startsWith(`${item.href}/`);
|
||||
|
||||
<div className="border-t border-surface-border p-4">
|
||||
<p className="text-xs text-text-muted">Mosaic Stack v0.0.4</p>
|
||||
</div>
|
||||
</aside>
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
className={cn(
|
||||
'group flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm transition-all duration-150',
|
||||
isActive ? 'font-medium' : 'hover:bg-white/4',
|
||||
)}
|
||||
style={
|
||||
isActive
|
||||
? {
|
||||
background: 'color-mix(in srgb, var(--primary) 18%, transparent)',
|
||||
color: 'var(--primary)',
|
||||
}
|
||||
: { color: 'var(--text-2)' }
|
||||
}
|
||||
>
|
||||
<span className="shrink-0" aria-hidden="true">
|
||||
{item.icon}
|
||||
</span>
|
||||
<span>{item.label}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className="border-t px-5 py-4" style={{ borderColor: 'var(--border)' }}>
|
||||
<p className="text-xs text-[var(--muted)]">Mosaic Stack v0.0.4</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{mobileOpen ? (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Close sidebar"
|
||||
className="fixed inset-0 z-40 bg-black/40 md:hidden"
|
||||
onClick={() => setMobileOpen(false)}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user