modify app layout
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-07-07 10:15:35 +09:00
parent 6b6587fcc4
commit 495f7b716f
4 changed files with 79 additions and 2190 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 376 KiB

+50 -2032
View File
File diff suppressed because it is too large Load Diff
+1 -155
View File
@@ -1,158 +1,4 @@
import { useMemo, useState, type ReactNode } from 'react';
import { Bell, ChevronDown, ChevronRight, Menu, Search, UserRound } from 'lucide-react';
import { Button } from '../components/ui';
import { cn } from '../lib/utils';
import { navGroups } from './navigation';
type AppLayoutProps = {
children: ReactNode;
currentPath: string;
onNavigate: (path: string) => void;
};
function isChildActive(item: { path?: string; children?: Array<{ path: string }> }, currentPath: string) {
if (item.path === currentPath) return true;
return item.children?.some((child) => child.path === currentPath) ?? false;
}
export function AppLayout({ children, currentPath, onNavigate }: AppLayoutProps) {
const [openGroups, setOpenGroups] = useState<Record<string, boolean>>({});
const [menuSearch, setMenuSearch] = useState('');
const normalizedSearch = menuSearch.trim().toLowerCase();
const visibleGroups = useMemo(() => {
if (!normalizedSearch) return navGroups;
return navGroups
.map((group) => ({
...group,
items: group.items.flatMap((item) => {
const parentMatched = item.label.toLowerCase().includes(normalizedSearch);
if ('children' in item && item.children) {
const children = item.children.filter((child) => child.label.toLowerCase().includes(normalizedSearch));
if (parentMatched) return [item];
if (children.length) return [{ ...item, children }];
return [];
}
return parentMatched ? [item] : [];
})
}))
.filter((group) => group.items.length > 0);
}, [normalizedSearch]);
return (
<div className="min-h-screen overflow-hidden bg-background text-foreground">
<div className="min-h-screen lg:pl-[17rem]">
<aside className="fixed inset-y-0 left-0 hidden w-[17rem] flex-col border-r border-border bg-sidebar px-3 py-4 lg:flex">
<div className="mb-8 flex shrink-0 justify-center px-4 py-3">
<img className="h-5 w-auto object-contain" src="/assets/brand/header.webp" alt="Autonomy Studio" />
</div>
<nav className="scrollbar-stable min-h-0 flex-1 space-y-5 overflow-y-auto pr-2">
{visibleGroups.map((group) => (
<div key={group.label}>
<p className="mb-2 px-2 text-xs font-medium text-muted-foreground">{group.label}</p>
<div className="space-y-1">
{group.items.map((item) => {
const Icon = item.icon;
const children = item.children;
const hasChildren = Boolean(children?.length);
const active = isChildActive(item, currentPath);
const expanded = Boolean(normalizedSearch) || openGroups[item.label] || active;
return (
<div key={item.label}>
<button
type="button"
onClick={() => {
if (hasChildren) {
setOpenGroups((prev) => ({ ...prev, [item.label]: !prev[item.label] }));
return;
}
if ('path' in item && item.path) onNavigate(item.path);
}}
className={cn(
'flex h-9 w-full items-center gap-2 rounded-md px-2 text-sm text-muted-foreground transition-colors hover:bg-sidebar-accent hover:text-white',
active && 'font-semibold text-white'
)}
>
<Icon className={cn('size-4', active && 'text-white')} />
<span className="min-w-0 flex-1 text-left">{item.label}</span>
{hasChildren ? (
expanded ? <ChevronDown className="size-3.5" /> : <ChevronRight className="size-3.5" />
) : null}
</button>
{hasChildren && expanded && children ? (
<div className="mt-1 space-y-1 pl-6">
{children.map((child) => {
const childActive = child.path === currentPath;
return (
<button
type="button"
key={child.path}
onClick={() => onNavigate(child.path)}
className={cn(
'flex h-8 w-full items-center gap-2 rounded-md px-2 text-left text-xs font-bold text-muted-foreground transition-colors hover:bg-sidebar-accent hover:text-white',
childActive && 'text-white'
)}
>
<span className={cn('h-3 w-1 rounded-full', childActive ? 'bg-white' : 'bg-transparent')} />
<span>{child.label}</span>
</button>
);
})}
</div>
) : null}
</div>
);
})}
</div>
</div>
))}
</nav>
</aside>
<div className="flex h-screen min-w-0 flex-col">
<header className="sticky top-0 z-20 flex h-16 items-center justify-between border-b border-border bg-background/90 px-4 backdrop-blur-xl md:px-6">
<div className="flex min-w-0 items-center gap-3">
<Button variant="outline" className="lg:hidden">
<Menu className="size-4" />
</Button>
<div className="relative hidden w-80 md:block">
<Search className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<input
className="h-9 w-full rounded-md border border-input bg-background pl-9 pr-3 text-sm outline-none placeholder:text-muted-foreground focus:ring-2 focus:ring-ring"
placeholder="메뉴 검색"
value={menuSearch}
onChange={(event) => setMenuSearch(event.target.value)}
/>
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="ghost" className="size-9 px-0">
<Bell className="size-4" />
</Button>
<Button variant="outline" className="gap-2">
<span className="flex size-6 items-center justify-center rounded bg-muted text-xs font-semibold">
<UserRound className="size-3.5" />
</span>
<span className="hidden md:inline">관리자</span>
<ChevronDown className="size-4 text-muted-foreground" />
</Button>
</div>
</header>
<main className="scrollbar-stable min-h-0 flex-1 overflow-y-auto">
<div className="mx-auto w-full max-w-[1520px] px-6 pb-16 pt-6 md:px-10 md:pb-20 xl:px-14">
{children}
</div>
</main>
</div>
</div>
</div>
);
}
import type { ReactNode } from 'react';
export function PageTitle({
eyebrow,
+28 -3
View File
@@ -226,10 +226,34 @@ function DiffViewer({ diff }: { diff: string }) {
const files = parseDiffFiles(diff);
return (
<div className="max-h-[56vh] overflow-auto rounded-md border border-border bg-[#080b10] p-3 font-mono text-xs leading-5">
<div className="space-y-4">
<div className="grid max-h-[56vh] overflow-hidden rounded-md border border-border bg-[#080b10] text-xs leading-5 lg:grid-cols-[280px_minmax(0,1fr)]">
<aside className="border-b border-slate-700/80 bg-[#0b0f14] lg:border-b-0 lg:border-r">
<div className="border-b border-slate-700/80 bg-[#111827] px-3 py-2">
<p className="text-[12px] font-semibold text-slate-100">변경 파일</p>
<p className="mt-0.5 text-[11px] text-slate-400">총 {files.length}개</p>
</div>
<div className="max-h-44 overflow-y-auto p-2 lg:max-h-[calc(56vh-50px)]">
{files.map((file) => (
<button
key={`tree-${file.id}`}
type="button"
className="flex w-full items-start justify-between gap-2 rounded px-2 py-1.5 text-left text-slate-300 transition-colors hover:bg-slate-800/80 hover:text-slate-50"
onClick={() => document.getElementById(`diff-file-${file.id}`)?.scrollIntoView({ behavior: 'smooth', block: 'start' })}
title={file.path}
>
<span className="min-w-0 flex-1 truncate font-mono text-[12px]">{file.path}</span>
<span className="flex shrink-0 items-center gap-1 font-mono text-[11px]">
<span className="text-emerald-300">+{file.added}</span>
<span className="text-rose-300">-{file.removed}</span>
</span>
</button>
))}
</div>
</aside>
<div className="min-w-0 overflow-auto p-3 font-mono">
<div className="space-y-4">
{files.map((file) => (
<section key={file.id} className="min-w-max overflow-hidden rounded-md border border-slate-700/80 bg-[#0b0f14]">
<section id={`diff-file-${file.id}`} key={file.id} className="min-w-max scroll-mt-3 overflow-hidden rounded-md border border-slate-700/80 bg-[#0b0f14]">
<div className="sticky top-0 z-10 flex items-center justify-between gap-4 border-b border-slate-700/80 bg-[#111827] px-4 py-2">
<div className="min-w-0">
<p className="truncate text-[13px] font-semibold text-slate-100">{file.path}</p>
@@ -249,6 +273,7 @@ function DiffViewer({ diff }: { diff: string }) {
))}
</section>
))}
</div>
</div>
</div>
);