nnuer
강희태@gmlxo9917

사용량 카드가 달린 업무용 앱 사이드바

내비게이션 블록

로고, 아이콘 메뉴 8개, 하단 워크스페이스 사용량 카드(저장공간·AI 크레딧 막대, 업그레이드 버튼)가 있는 흰색 세로 사이드바입니다. 모바일에서는 메뉴 버튼으로 접고 펼칩니다.

블록 신고

운영자가 확인한 뒤 필요하면 블록을 숨깁니다.

사유
Claude Code · 모름 · React (TSX)
결과 코드펼치기
import { useState } from "react";

const FONT_URL = "https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&display=swap";

type IconProps = { className?: string };

function Svg({ className, children }: IconProps & { children: React.ReactNode }) {
  return (
    <svg className={className} width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      {children}
    </svg>
  );
}

const icons = {
  logo: (p: IconProps) => (
    <Svg {...p}>
      <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>
  ),
  dashboard: (p: IconProps) => (
    <Svg {...p}>
      <rect x="3" y="3" width="7" height="9" rx="1" />
      <rect x="14" y="3" width="7" height="5" rx="1" />
      <rect x="14" y="12" width="7" height="9" rx="1" />
      <rect x="3" y="16" width="7" height="5" rx="1" />
    </Svg>
  ),
  notes: (p: IconProps) => (
    <Svg {...p}>
      <path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" />
      <path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" />
    </Svg>
  ),
  cpu: (p: IconProps) => (
    <Svg {...p}>
      <rect x="4" y="4" width="16" height="16" rx="2" />
      <rect x="9" y="9" width="6" height="6" />
      <path d="M9 1v3M15 1v3M9 20v3M15 20v3M20 9h3M20 14h3M1 9h3M1 14h3" />
    </Svg>
  ),
  layers: (p: IconProps) => (
    <Svg {...p}>
      <path d="M12 2 2 7l10 5 10-5-10-5z" />
      <path d="m2 17 10 5 10-5" />
      <path d="m2 12 10 5 10-5" />
    </Svg>
  ),
  chart: (p: IconProps) => (
    <Svg {...p}>
      <path d="M12 20V10M18 20V4M6 20v-4" />
    </Svg>
  ),
  printer: (p: IconProps) => (
    <Svg {...p}>
      <path d="M6 9V2h12v7" />
      <path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2" />
      <rect x="6" y="14" width="12" height="8" />
    </Svg>
  ),
  users: (p: IconProps) => (
    <Svg {...p}>
      <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
      <circle cx="9" cy="7" r="4" />
      <path d="M23 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75" />
    </Svg>
  ),
  bell: (p: IconProps) => (
    <Svg {...p}>
      <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>
  ),
  menu: (p: IconProps) => (
    <Svg {...p}>
      <path d="M3 6h18M3 12h18M3 18h18" />
    </Svg>
  ),
};

const NAV = [
  { id: "dashboard", label: "대시보드", icon: icons.dashboard },
  { id: "notes", label: "노트 관리", icon: icons.notes },
  { id: "ai", label: "AI 도구", icon: icons.cpu },
  { id: "assets", label: "콘텐츠 & 에셋", icon: icons.layers },
  { id: "analytics", label: "분석", icon: icons.chart },
  { id: "print", label: "인쇄", icon: icons.printer },
  { id: "members", label: "멤버 & 권한", icon: icons.users },
  { id: "alerts", label: "알림", icon: icons.bell },
];

const RAINBOW =
  "linear-gradient(90deg, #ffcccc 0%, #ffd6c4 10%, #ffe5bc 20%, #f6ffbc 30%, #d7ffc3 40%, #b6ffec 50%, #bbf7ff 60%, #b7bdff 70%, #d3adff 80%, #ffb6e0 90%, #ffb6b7 95%, #ffdac2 100%)";

function UsageBar({ label, value, fill }: { label: string; value: string; fill: string }) {
  return (
    <div className="flex flex-col gap-1.5">
      <div className="flex items-start justify-between text-[11px] leading-normal">
        <span className="font-normal text-[#6f767e]">{label}</span>
        <span className="font-semibold text-[#1a1d1f]">{value}</span>
      </div>
      <div className="h-1.5 w-full overflow-hidden rounded-[3px] bg-[#efefef]" role="meter" aria-label={label} aria-valuemin={0} aria-valuemax={100} aria-valuenow={100}>
        <div className="h-full w-full" style={{ background: fill }} />
      </div>
    </div>
  );
}

export default function AppSidebar() {
  const [active, setActive] = useState("dashboard");
  const [open, setOpen] = useState(true);

  return (
    <section className="w-full bg-[#f4f5f7] p-4 font-['Geist',sans-serif] sm:p-6">
      <link rel="stylesheet" href={FONT_URL} precedence="default" />
      <div className="mx-auto flex w-full max-w-[1200px] flex-col gap-4 md:flex-row">
        <aside className="flex w-full flex-col justify-between rounded-2xl border-r border-[#efefef] bg-white px-4 py-6 md:min-h-[760px] md:w-[220px] md:shrink-0 md:py-8">
          <div className="flex flex-col gap-8">
            <div className="flex items-center justify-between">
              <div className="flex items-center gap-2 pl-2">
                <span className="flex size-8 items-center justify-center rounded-lg bg-[#5b8def] text-white">
                  <icons.logo />
                </span>
                <span className="text-[18px] font-bold leading-normal text-[#1a1d1f]">Leafnote</span>
              </div>
              <button
                type="button"
                onClick={() => setOpen((v) => !v)}
                aria-expanded={open}
                aria-controls="sidebar-panel"
                aria-label="메뉴 열기"
                className="rounded-lg p-2 text-[#6f767e] hover:bg-[#f7f8fa] focus-visible:outline-2 focus-visible:outline-[#5b8def] md:hidden"
              >
                <icons.menu />
              </button>
            </div>
            <nav id="sidebar-panel" aria-label="주요 메뉴" className={`${open ? "flex" : "hidden"} flex-col gap-1 md:flex`}>
              {NAV.map((item) => {
                const isActive = active === item.id;
                const Icon = item.icon;
                return (
                  <button
                    key={item.id}
                    type="button"
                    onClick={() => setActive(item.id)}
                    aria-current={isActive ? "page" : undefined}
                    className={`flex w-full items-center gap-3 rounded-xl px-4 py-3 text-left text-[14px] leading-normal transition-colors duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#5b8def] ${
                      isActive ? "bg-[#5b8def] font-semibold text-white" : "font-medium text-[#6f767e] hover:bg-[#f7f8fa] hover:text-[#1a1d1f]"
                    }`}
                  >
                    <Icon className="shrink-0" />
                    <span className="flex-1">{item.label}</span>
                  </button>
                );
              })}
            </nav>
          </div>
          <div className={`${open ? "flex" : "hidden"} mt-8 flex-col gap-3 rounded-2xl bg-[#f7f8fa] p-4 md:flex`}>
            <div className="flex items-center justify-between">
              <span className="text-[13px] font-semibold leading-normal text-[#1a1d1f]">Demo Workspace</span>
              <span className="rounded px-2 py-0.5 text-[11px] font-semibold leading-normal text-[#27ae60]" style={{ background: "rgba(39,174,96,0.08)" }}>
                PRO
              </span>
            </div>
            <UsageBar label="저장공간" value="14.8K / 14.8K" fill="#5b8def" />
            <UsageBar label="AI 크레딧" value="14.8K / 14.8K" fill={RAINBOW} />
            <button
              type="button"
              className="w-full rounded-lg bg-[#5b8def] py-2 text-[13px] font-semibold leading-normal text-white transition-colors hover:bg-[#3b82f6] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#5b8def]"
            >
              업그레이드
            </button>
          </div>
        </aside>
        <div className="hidden flex-1 rounded-2xl border border-dashed border-[#e5e7eb] md:block" aria-hidden="true" />
      </div>
    </section>
  );
}

적용 사이트 0

이 블록이 실제로 쓰인 사이트입니다.

로그인하고 내 사이트 추가

아직 등록된 사이트가 없습니다. 이 블록을 쓴 사이트가 있다면 알려 주세요.

로그인하고 재현 결과 올리기

재현 후기

v1 · 0개

아직 이 버전의 프롬프트로 직접 만들어 본 사람이 없습니다. 첫 번째로 재현해 보세요.