feat(ui): 카드 그라디언트 헤어라인 + 앰비언트 글로우 (레퍼런스 #5)

- glass.borderGradient: 좌상단 발광→우하단 소멸 1px 링 (mask exclude 기법 —
  반투명 글래스 표면에서도 정확)
- glow.card/cardHover: 블루 앰비언트 상단 글로우 + 깊이 그림자 결합
- sheen 재정의: 좌상단 라디얼 앰비언트 + 수직 시인 레이어
- 6개 테마 전부 액센트색 파생, MetalCard/InstrumentPanel/MuiCard 적용
This commit is contained in:
Yun Chan 2026-07-21 20:36:25 +09:00
parent 082ed2ef02
commit e0a1864e60
4 changed files with 102 additions and 23 deletions

View file

@ -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(--) 주입 구조 유지)

View file

@ -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,
},
}}
>
{/* 모서리 메타 라벨 */}

View file

@ -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,
}}
>

View file

@ -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<ThemeKey, RawTheme> = {
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<ThemeKey, RawTheme> = {
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<ThemeKey, RawTheme> = {
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<ThemeKey, RawTheme> = {
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<ThemeKey, RawTheme> = {
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<ThemeKey, RawTheme> = {
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<ThemeKey, RawTheme> = {
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<ThemeKey, RawTheme> = {
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<ThemeKey, RawTheme> = {
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<ThemeKey, RawTheme> = {
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<ThemeKey, RawTheme> = {
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<ThemeKey, RawTheme> = {
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<string, string> {
'--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<string, string> {
'--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)' },
},
},
},