diff --git a/memory/project_status.md b/memory/project_status.md index 644418a..04d37f5 100644 --- a/memory/project_status.md +++ b/memory/project_status.md @@ -18,6 +18,10 @@ - **StatusBar**: 브랜드+버전+모델 / 로컬 시간 시계 - 검증: tsc 0, vitest 41/41, 실행 스크린샷으로 대시보드/히스토리/대화/회의/지식/설정 전 페이지 확인. 스크롤 시 폭 불변 확인. 상단 1px 잡티는 detached DevTools 겹침(앱 버그 아님) +### 카드 글로우 고도화 (사용자 레퍼런스 #5) +- `glass.borderGradient`(좌상단 발광→우하단 소멸 그라디언트 헤어라인, mask 링 기법 ::after) + `glow.card/cardHover`(블루 앰비언트+깊이 그림자) + `sheen` 재정의(라디얼 앰비언트+수직 시인 레이어) — 6개 테마 전부 액센트 파생 +- 적용: MetalCard, InstrumentPanel, MuiCard 오버라이드. 반투명 표면에서도 정확한 1px 그라디언트 링 (padding-box 기법 대신 mask exclude 사용 — 반투명 배경 투과 문제 회피) + ### 마감 완료 (워크플로 8 에이전트 병렬) 1. ✅ @mui/icons-material → lucide-react 전환 (23파일, 잔여 0 — 의존성 제거는 추후) 2. ✅ Vanilla 팝업 5종 CSS v2 정합 (글래스 네이비 + 블루 액센트, var(--) 주입 구조 유지) diff --git a/packages/ui/src/components/ds/InstrumentPanel.tsx b/packages/ui/src/components/ds/InstrumentPanel.tsx index 7d89375..f5df3cf 100644 --- a/packages/ui/src/components/ds/InstrumentPanel.tsx +++ b/packages/ui/src/components/ds/InstrumentPanel.tsx @@ -30,20 +30,33 @@ export function InstrumentPanel({ bgcolor: d3roPalette.glass.surface, backdropFilter: `blur(${d3roPalette.glass.blur})`, borderRadius: d3roRadius.outer, - border: `1px solid ${d3roPalette.glass.hairline}`, p: 3, - boxShadow: d3roShadow.card, + boxShadow: d3roShadow.glowCard, overflow: 'hidden', - // 앰비언트 액센트 글로우 (좌상단 은은한 빛) + // 앰비언트 시인 (좌상단 라디얼 + 상단 광택 — theme sheen 토큰) '&::before': { content: '""', position: 'absolute', inset: 0, borderRadius: 'inherit', - background: `radial-gradient(ellipse 60% 40% at 18% 0%, ${d3roPalette.accent.dim} 0%, transparent 60%)`, + background: d3roPalette.glass.sheen, pointerEvents: 'none', zIndex: 1, }, + // 그라디언트 헤어라인 링 + '&::after': { + content: '""', + position: 'absolute', + inset: 0, + borderRadius: 'inherit', + padding: '1px', + background: d3roPalette.glass.borderGradient, + WebkitMask: 'linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)', + WebkitMaskComposite: 'xor', + maskComposite: 'exclude', + pointerEvents: 'none', + zIndex: 2, + }, }} > {/* 모서리 메타 라벨 */} diff --git a/packages/ui/src/components/ds/MetalCard.tsx b/packages/ui/src/components/ds/MetalCard.tsx index db2899c..511f08c 100644 --- a/packages/ui/src/components/ds/MetalCard.tsx +++ b/packages/ui/src/components/ds/MetalCard.tsx @@ -1,8 +1,9 @@ 'use client' -// packages/ui/src/components/ds/MetalCard.tssx -// v2 "Midnight Glass": 글래스모피즘 카드 — 반투명 표면 + 옅은 헤어라인 + 상단 시인 -// 토큰 적용: d3roPalette.glass, d3roShadow, d3roRadius +// packages/ui/src/components/ds/MetalCard.tsx +// v2 "Midnight Glass": 글래스모피즘 카드 — +// 그라디언트 헤어라인 보더(좌상단 발광 → 우하단 소멸) + 앰비언트 글로우 + 상단 시인. +// 보더는 mask 링 기법(::after) — 반투명 표면 위에서도 정확한 1px 그라디언트 링. import { Box, type BoxProps } from '@mui/material' import { d3roPalette, d3roShadow, d3roRadius } from '../../theme' @@ -26,12 +27,12 @@ export function MetalCard({ bgcolor: inset ? d3roPalette.glass.raised : d3roPalette.glass.surface, backdropFilter: `blur(${d3roPalette.glass.blur})`, borderRadius: inset ? d3roRadius.inner : d3roRadius.card, - border: `1px solid ${d3roPalette.glass.hairline}`, - boxShadow: inset ? 'none' : d3roShadow.card, + border: inset ? `1px solid ${d3roPalette.glass.hairline}` : 'none', + boxShadow: inset ? 'none' : d3roShadow.glowCard, p: inset ? 2 : 3, overflow: 'hidden', - transition: 'border-color 0.2s ease', - // 상단 시인(광택) 오버레이 + transition: 'box-shadow 0.25s ease', + // 상단 시인 + 앰비언트 라디얼 (theme sheen 토큰이 레이어 결합) '&::before': inset ? {} : { @@ -42,7 +43,22 @@ export function MetalCard({ background: d3roPalette.glass.sheen, pointerEvents: 'none', }, - '&:hover': inset ? {} : { borderColor: d3roPalette.glass.hairlineStrong }, + // 그라디언트 헤어라인 링 — padding 1px + mask exclude로 링만 남김 + '&::after': inset + ? {} + : { + content: '""', + position: 'absolute', + inset: 0, + borderRadius: 'inherit', + padding: '1px', + background: d3roPalette.glass.borderGradient, + WebkitMask: 'linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)', + WebkitMaskComposite: 'xor', + maskComposite: 'exclude', + pointerEvents: 'none', + }, + '&:hover': inset ? {} : { boxShadow: d3roShadow.glowCardHover }, ...sx, }} > diff --git a/packages/ui/src/theme.ts b/packages/ui/src/theme.ts index a62a6ba..3adf5b6 100644 --- a/packages/ui/src/theme.ts +++ b/packages/ui/src/theme.ts @@ -74,8 +74,10 @@ interface RawTheme { /** 헤어라인 보더 */ hairline: string hairlineStrong: string - /** 카드 상단 시인(광택) 오버레이 */ + /** 카드 상단 시인(광택) 오버레이 — 앰비언트 라디얼 + 수직 시인 레이어 */ sheen: string + /** 그라디언트 헤어라인 보더 — 좌상단이 밝고 우하단으로 소멸 (mask 링에 사용) */ + borderGradient: string /** backdrop-filter blur 반경 */ blur: string } @@ -100,6 +102,10 @@ interface RawTheme { /** 프라이머리 버튼/액티브 글로우 */ accent: string soft: string + /** 카드 외곽 앰비언트 글로우 (+깊이 그림자 결합) */ + card: string + /** 카드 hover 시 강화 글로우 */ + cardHover: string } } @@ -128,7 +134,8 @@ const RAW: Record = { raised: 'rgba(26,38,68,0.55)', hairline: 'rgba(148,180,255,0.10)', hairlineStrong: 'rgba(148,180,255,0.18)', - sheen: 'linear-gradient(180deg, rgba(148,180,255,0.06) 0%, rgba(148,180,255,0) 42%)', + sheen: 'radial-gradient(ellipse 70% 55% at 18% -8%, rgba(59,130,246,0.14) 0%, transparent 55%), linear-gradient(180deg, rgba(148,180,255,0.05) 0%, rgba(148,180,255,0) 40%)', + borderGradient: 'linear-gradient(135deg, rgba(125,180,255,0.55) 0%, rgba(148,180,255,0.14) 32%, rgba(148,180,255,0.06) 58%, rgba(59,130,246,0.22) 100%)', blur: '24px', }, gradient: { @@ -141,6 +148,8 @@ const RAW: Record = { glow: { accent: '0 0 0 1px rgba(59,130,246,0.35), 0 8px 24px rgba(37,99,235,0.35)', soft: '0 0 16px rgba(59,130,246,0.25)', + card: '0 -8px 32px -12px rgba(59,130,246,0.22), 0 12px 40px rgba(3,7,18,0.45)', + cardHover: '0 -10px 40px -10px rgba(59,130,246,0.34), 0 12px 40px rgba(3,7,18,0.5)', }, }, // ── Light — 블루 액센트 유지, 밝은 글래스 ──────────────────── @@ -165,7 +174,8 @@ const RAW: Record = { raised: 'rgba(255,255,255,0.85)', hairline: 'rgba(30,58,138,0.10)', hairlineStrong: 'rgba(30,58,138,0.18)', - sheen: 'linear-gradient(180deg, rgba(255,255,255,0.6) 0%, rgba(255,255,255,0) 42%)', + sheen: 'radial-gradient(ellipse 70% 55% at 18% -8%, rgba(37,99,235,0.08) 0%, transparent 55%), linear-gradient(180deg, rgba(255,255,255,0.6) 0%, rgba(255,255,255,0) 42%)', + borderGradient: 'linear-gradient(135deg, rgba(37,99,235,0.45) 0%, rgba(30,58,138,0.14) 32%, rgba(30,58,138,0.08) 58%, rgba(37,99,235,0.18) 100%)', blur: '24px', }, gradient: { @@ -178,6 +188,8 @@ const RAW: Record = { glow: { accent: '0 0 0 1px rgba(37,99,235,0.25), 0 8px 24px rgba(37,99,235,0.25)', soft: '0 0 16px rgba(37,99,235,0.18)', + card: '0 -6px 28px -12px rgba(37,99,235,0.18), 0 8px 30px rgba(30,58,138,0.08)', + cardHover: '0 -8px 36px -10px rgba(37,99,235,0.28), 0 8px 30px rgba(30,58,138,0.10)', }, }, // ── Nord (https://www.nordtheme.com/) ────────────────────── @@ -202,7 +214,8 @@ const RAW: Record = { raised: 'rgba(59,66,82,0.55)', hairline: 'rgba(216,222,233,0.09)', hairlineStrong: 'rgba(216,222,233,0.16)', - sheen: 'linear-gradient(180deg, rgba(216,222,233,0.05) 0%, rgba(216,222,233,0) 42%)', + sheen: 'radial-gradient(ellipse 70% 55% at 18% -8%, rgba(136,192,208,0.14) 0%, transparent 55%), linear-gradient(180deg, rgba(216,222,233,0.05) 0%, rgba(216,222,233,0) 40%)', + borderGradient: 'linear-gradient(135deg, rgba(160,205,220,0.55) 0%, rgba(216,222,233,0.14) 32%, rgba(216,222,233,0.06) 58%, rgba(136,192,208,0.22) 100%)', blur: '24px', }, gradient: { @@ -215,6 +228,8 @@ const RAW: Record = { glow: { accent: '0 0 0 1px rgba(136,192,208,0.3), 0 8px 24px rgba(94,129,172,0.35)', soft: '0 0 16px rgba(136,192,208,0.22)', + card: '0 -8px 32px -12px rgba(136,192,208,0.22), 0 12px 40px rgba(3,7,18,0.45)', + cardHover: '0 -10px 40px -10px rgba(136,192,208,0.34), 0 12px 40px rgba(3,7,18,0.5)', }, }, // ── Solarized Dark (https://ethanschoonover.com/solarized/) ─ @@ -239,7 +254,8 @@ const RAW: Record = { raised: 'rgba(13,66,80,0.55)', hairline: 'rgba(131,148,150,0.11)', hairlineStrong: 'rgba(131,148,150,0.19)', - sheen: 'linear-gradient(180deg, rgba(238,232,213,0.04) 0%, rgba(238,232,213,0) 42%)', + sheen: 'radial-gradient(ellipse 70% 55% at 18% -8%, rgba(181,137,0,0.14) 0%, transparent 55%), linear-gradient(180deg, rgba(131,148,150,0.05) 0%, rgba(131,148,150,0) 40%)', + borderGradient: 'linear-gradient(135deg, rgba(212,160,23,0.55) 0%, rgba(131,148,150,0.14) 32%, rgba(131,148,150,0.06) 58%, rgba(181,137,0,0.22) 100%)', blur: '24px', }, gradient: { @@ -252,6 +268,8 @@ const RAW: Record = { glow: { accent: '0 0 0 1px rgba(181,137,0,0.3), 0 8px 24px rgba(181,137,0,0.3)', soft: '0 0 16px rgba(181,137,0,0.22)', + card: '0 -8px 32px -12px rgba(181,137,0,0.22), 0 12px 40px rgba(3,7,18,0.45)', + cardHover: '0 -10px 40px -10px rgba(181,137,0,0.34), 0 12px 40px rgba(3,7,18,0.5)', }, }, // ── Catppuccin Mocha (https://catppuccin.com/) ────────────── @@ -276,7 +294,8 @@ const RAW: Record = { raised: 'rgba(49,50,68,0.55)', hairline: 'rgba(205,214,244,0.08)', hairlineStrong: 'rgba(205,214,244,0.15)', - sheen: 'linear-gradient(180deg, rgba(205,214,244,0.05) 0%, rgba(205,214,244,0) 42%)', + sheen: 'radial-gradient(ellipse 70% 55% at 18% -8%, rgba(203,166,247,0.14) 0%, transparent 55%), linear-gradient(180deg, rgba(205,214,244,0.05) 0%, rgba(205,214,244,0) 40%)', + borderGradient: 'linear-gradient(135deg, rgba(215,185,255,0.55) 0%, rgba(205,214,244,0.14) 32%, rgba(205,214,244,0.06) 58%, rgba(203,166,247,0.22) 100%)', blur: '24px', }, gradient: { @@ -289,6 +308,8 @@ const RAW: Record = { glow: { accent: '0 0 0 1px rgba(203,166,247,0.3), 0 8px 24px rgba(203,166,247,0.3)', soft: '0 0 16px rgba(203,166,247,0.22)', + card: '0 -8px 32px -12px rgba(203,166,247,0.22), 0 12px 40px rgba(3,7,18,0.45)', + cardHover: '0 -10px 40px -10px rgba(203,166,247,0.34), 0 12px 40px rgba(3,7,18,0.5)', }, }, // ── Dracula (https://draculatheme.com/) ───────────────────── @@ -313,7 +334,8 @@ const RAW: Record = { raised: 'rgba(56,58,74,0.55)', hairline: 'rgba(248,248,242,0.08)', hairlineStrong: 'rgba(248,248,242,0.15)', - sheen: 'linear-gradient(180deg, rgba(248,248,242,0.05) 0%, rgba(248,248,242,0) 42%)', + sheen: 'radial-gradient(ellipse 70% 55% at 18% -8%, rgba(189,147,249,0.14) 0%, transparent 55%), linear-gradient(180deg, rgba(248,248,242,0.05) 0%, rgba(248,248,242,0) 40%)', + borderGradient: 'linear-gradient(135deg, rgba(205,170,255,0.55) 0%, rgba(248,248,242,0.14) 32%, rgba(248,248,242,0.06) 58%, rgba(189,147,249,0.22) 100%)', blur: '24px', }, gradient: { @@ -326,6 +348,8 @@ const RAW: Record = { glow: { accent: '0 0 0 1px rgba(189,147,249,0.3), 0 8px 24px rgba(189,147,249,0.3)', soft: '0 0 16px rgba(189,147,249,0.22)', + card: '0 -8px 32px -12px rgba(189,147,249,0.22), 0 12px 40px rgba(3,7,18,0.45)', + cardHover: '0 -10px 40px -10px rgba(189,147,249,0.34), 0 12px 40px rgba(3,7,18,0.5)', }, }, } @@ -364,6 +388,7 @@ export const d3roPalette = { hairline: 'var(--d3-glass-hairline)', hairlineStrong: 'var(--d3-glass-hairlineStrong)', sheen: 'var(--d3-glass-sheen)', + borderGradient: 'var(--d3-glass-borderGradient)', blur: 'var(--d3-glass-blur)', }, /** v2: 그라디언트 토큰 */ @@ -381,6 +406,8 @@ export const d3roPalette = { glow: { accent: 'var(--d3-glow-accent)', soft: 'var(--d3-glow-soft)', + card: 'var(--d3-glow-card)', + cardHover: 'var(--d3-glow-cardHover)', }, tag: { purple: '#a78bfa', @@ -487,6 +514,8 @@ export const d3roShadow = { /** v2: 액센트 글로우 */ glowAccent: 'var(--d3-glow-accent)', glowSoft: 'var(--d3-glow-soft)', + glowCard: 'var(--d3-glow-card)', + glowCardHover: 'var(--d3-glow-cardHover)', } as const // ── SSOT: 반경 토큰 ──────────────────────────────────── @@ -547,6 +576,7 @@ function buildCssVars(key: ThemeKey): Record { '--d3-glass-hairline': r.glass.hairline, '--d3-glass-hairlineStrong': r.glass.hairlineStrong, '--d3-glass-sheen': r.glass.sheen, + '--d3-glass-borderGradient': r.glass.borderGradient, '--d3-glass-blur': r.glass.blur, '--d3-gradient-accent': r.gradient.accent, '--d3-gradient-logo': r.gradient.logo, @@ -558,6 +588,8 @@ function buildCssVars(key: ThemeKey): Record { '--d3-gradient-wave4': r.gradient.wave4, '--d3-glow-accent': r.glow.accent, '--d3-glow-soft': r.glow.soft, + '--d3-glow-card': r.glow.card, + '--d3-glow-cardHover': r.glow.cardHover, } } @@ -642,13 +674,27 @@ function createD3ROTheme(key: ThemeKey): Theme { defaultProps: { elevation: 0 }, styleOverrides: { root: { + position: 'relative', backgroundColor: 'var(--d3-glass-surface)', backdropFilter: 'blur(var(--d3-glass-blur))', borderRadius: 16, - border: `1px solid ${r.glass.hairline}`, - boxShadow: r.shadow.card, - transition: 'border-color 0.2s ease', - '&:hover': { borderColor: r.glass.hairlineStrong }, + boxShadow: 'var(--d3-glow-card)', + transition: 'box-shadow 0.25s ease', + overflow: 'hidden', + // 그라디언트 헤어라인 링 (MetalCard와 동일 기법) + '&::after': { + content: '""', + position: 'absolute', + inset: 0, + borderRadius: 'inherit', + padding: '1px', + background: 'var(--d3-glass-borderGradient)', + WebkitMask: 'linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)', + WebkitMaskComposite: 'xor', + maskComposite: 'exclude', + pointerEvents: 'none', + }, + '&:hover': { boxShadow: 'var(--d3-glow-cardHover)' }, }, }, },