style(frontend): ABAP_OPENCODE 디자인 시스템으로 맞춤 + 화면 재구성

DESIGN_SYSTEM.md 의 Dark Workspace 토큰을 shadcn 토큰으로 이식(다크 기본, radius 8px,
success/warning/shadow-float), 시스템 산세리프, PwC 로고(LogoMark). 목록·새 대화·대화·
로그인 화면을 가운데 760px 열로 통일하고, 떠 있던 테마·로그아웃 상자는 상단 바로,
말풍선은 다크 토큰(AI 답변은 상자 없이), ABAP 흰 에디터 룩은 라이트 모드에서만.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-16 21:24:17 +09:00
co-authored by Claude Fable 5.1
parent 506fbf601d
commit 9c6b28782d
32 changed files with 490 additions and 353 deletions
@@ -4,8 +4,11 @@ import { invoke } from "@tauri-apps/api/core"
import { X } from "lucide-react"
import { toast } from "sonner"
import { useLocation } from "react-router-dom"
import { LogoMark } from "@/shared/components/LogoMark"
import { SnapUserControls } from "@/features/snap/components/SnapUserControls"
/** Tauri 프레임리스 창의 공통 제목줄. 닫기는 트레이로 숨김. */
/** Tauri 프레임리스 창의 공통 제목줄. 닫기는 트레이로 숨김.
* 로그인 뒤 화면(snap·snippet)에선 테마·로그아웃도 여기 오른쪽에 — 따로 떠 있는 상자 없이. */
export function DesktopWindowFrame({ children }: { children: ReactNode }) {
const { pathname } = useLocation()
const palette = pathname === "/snippet" || pathname === "/snap" || pathname.startsWith("/snap/")
@@ -18,29 +21,33 @@ export function DesktopWindowFrame({ children }: { children: ReactNode }) {
return (
<div className="bg-background text-foreground flex h-screen flex-col overflow-hidden">
<header
className={`flex shrink-0 items-center select-none ${palette ? "h-7" : "border-border h-9 border-b"}`}
className={`border-border flex h-8 shrink-0 items-center border-b select-none ${palette ? "" : "h-9"}`}
>
<div
className="text-muted-foreground flex h-full min-w-0 flex-1 items-center px-4 text-[10px] font-medium"
className="text-muted-foreground flex h-full min-w-0 flex-1 items-center gap-2 px-3 text-[10.5px] font-semibold tracking-wide"
onMouseDown={(event) => {
if (event.button === 0) run("window_drag")
}}
// 창 이동은 포인터 전용 동작이며 버튼과 영역을 분리함.
role="presentation"
>
<LogoMark width={22} />
CodeAssist
</div>
{palette && <SnapUserControls />}
<button
type="button"
aria-label="창 닫기"
title="닫기 (트레이로 숨기기)"
className="text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:ring-ring mr-1 flex h-full w-8 shrink-0 items-center justify-center rounded-md transition-colors focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-inset"
className="text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:ring-ring mr-1 ml-0.5 flex size-7 shrink-0 items-center justify-center rounded-md transition-colors focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-inset"
onClick={() => run("window_hide")}
>
<X className="size-4" aria-hidden="true" />
<X className="size-3.5" aria-hidden="true" />
</button>
</header>
<div className="min-h-0 flex-1 overflow-auto [&_.h-screen]:h-full">{children}</div>
<div className="min-h-0 flex-1 overflow-auto [&_.h-screen]:h-full [&_.min-h-screen]:min-h-full">
{children}
</div>
</div>
)
}
+16 -9
View File
@@ -1,19 +1,26 @@
import { Link, Outlet } from "react-router-dom"
import { hostKind } from "@/lib/bridge/transport"
import { PATHS } from "@/config/routes"
import { ThemeToggle } from "@/shared/components/ThemeToggle"
import { LogoMark } from "@/shared/components/LogoMark"
/** 로그인 등 공개 화면 셸. Tauri 에선 창 제목줄(DesktopWindowFrame)이 로고·테마를 이미 보여주므로 헤더를 안 그림. */
export default function Layout() {
const tauri = hostKind() === "tauri"
return (
<div className="flex min-h-screen flex-col">
<header className="border-b">
<div className="container flex h-14 items-center justify-between">
<Link to={PATHS.HOME} className="font-semibold">
Frontend Template
</Link>
<ThemeToggle />
</div>
</header>
<main className="container flex min-h-0 flex-1 flex-col py-8">
{!tauri && (
<header className="border-border border-b">
<div className="mx-auto flex h-9 w-full max-w-5xl items-center justify-between pr-2 pl-3">
<Link to={PATHS.HOME} className="flex items-center gap-2 text-[11px] font-semibold">
<LogoMark width={26} />
CodeAssist
</Link>
<ThemeToggle />
</div>
</header>
)}
<main className="mx-auto flex min-h-0 w-full max-w-5xl flex-1 flex-col px-4 py-8">
<Outlet />
</main>
</div>
@@ -0,0 +1,19 @@
import logo from "@/assets/logo.png"
import logoDark from "@/assets/logo-dark.png"
import { cn } from "@/lib/utils/cn"
interface LogoMarkProps {
/** 로고 가로 폭(px) — 원본 비율(약 2.06:1) 유지 */
width?: number
className?: string
}
/** 브랜드 마크 — ABAP_OPENCODE 와 같은 PwC 로고. 다크에선 밝은 글자 변형. */
export function LogoMark({ width = 38, className }: LogoMarkProps) {
return (
<span className={cn("inline-flex shrink-0", className)} style={{ width }}>
<img src={logo} alt="PwC" className="block h-auto w-full dark:hidden" />
<img src={logoDark} alt="PwC" className="hidden h-auto w-full dark:block" />
</span>
)
}
+2 -2
View File
@@ -35,8 +35,8 @@ const ORDER: Theme[] = ["light", "dark", "system"]
export const useThemeStore = create<ThemeState>()(
persist(
(set, get) => ({
theme: "light",
resolved: resolve("light"),
theme: "dark", // ABAP_OPENCODE 와 같이 다크가 기본
resolved: resolve("dark"),
setTheme: (theme) => {
const resolved = resolve(theme)
applyToDocument(resolved)
+24 -23
View File
@@ -83,34 +83,35 @@ export type DerivedTokens = {
// 의미색 — 모든 테마 공통
const DESTRUCTIVE = "oklch(0.577 0.245 27.325)"
// 사용자가 선택한 목업 색을 그대로 유지. 명도 정렬로 primary가 차콜로 바뀌지 않게 함.
// 기본 팔레트(id 는 하위호환으로 clean-blue 유지) — ABAP_OPENCODE DESIGN_SYSTEM.md §7 라이트 값.
// globals.css :root 와 같은 값. 명도 정렬(derive)을 안 타서 블루가 차콜로 안 바뀜.
const CLEAN_BLUE_TOKENS: DerivedTokens = {
background: "#FFFFFF",
foreground: "#242C39",
card: "#F7F9FC",
cardForeground: "#242C39",
background: "#FAFBFC",
foreground: "#1A1D22",
card: "#FFFFFF",
cardForeground: "#1A1D22",
popover: "#FFFFFF",
popoverForeground: "#242C39",
primary: "#034EA2",
popoverForeground: "#1A1D22",
primary: "#3563E9",
primaryForeground: "#FFFFFF",
secondary: "#F0F3F8",
secondaryForeground: "#242C39",
muted: "#F7F9FC",
mutedForeground: "#606D7F",
accent: "#EDF3FC",
accentForeground: "#034EA2",
secondary: "#EFF1F4",
secondaryForeground: "#1A1D22",
muted: "#F2F3F5",
mutedForeground: "#494F59",
accent: "#E4E8EF",
accentForeground: "#1A1D22",
destructive: DESTRUCTIVE,
border: "#E1E6EE",
input: "#BBC7D8",
ring: "#034EA2",
sidebar: "#F7F9FC",
sidebarForeground: "#242C39",
sidebarPrimary: "#034EA2",
border: "#E1E3E8",
input: "#E1E3E8",
ring: "#3563E9",
sidebar: "#F2F3F5",
sidebarForeground: "#1A1D22",
sidebarPrimary: "#3563E9",
sidebarPrimaryForeground: "#FFFFFF",
sidebarAccent: "#EDF3FC",
sidebarAccentForeground: "#034EA2",
sidebarBorder: "#E1E6EE",
sidebarRing: "#034EA2",
sidebarAccent: "#E9EBEF",
sidebarAccentForeground: "#1A1D22",
sidebarBorder: "#E9EBEF",
sidebarRing: "#3563E9",
}
export function derivePaletteTokens(
+3 -3
View File
@@ -10,9 +10,9 @@ export type RawPalette = {
export const PALETTES: RawPalette[] = [
{
id: "clean-blue",
name: "클린 블루",
swatch: ["#FFFFFF", "#F7F9FC", "#034EA2", "#242C39"],
id: "clean-blue", // id 는 저장된 값 호환 위해 유지 — 실제 토큰은 derive.ts 워크스페이스(ABAP_OPENCODE) 값
name: "워크스페이스",
swatch: ["#FAFBFC", "#F2F3F5", "#3563E9", "#1A1D22"],
},
// Sheet 1
{ id: "dark-teal", name: "Dark Teal", swatch: ["#1A222B", "#2D3540", "#009E9F", "#ECECEC"] },