feat(ui): Midnight Glass v2 전면 리디자인 — 기반+셸+대시보드

- theme.ts 재작성: glass/gradient/glow 토큰군, 기본 dark=Midnight(블루),
  6종 테마 변형 전부 파생 토큰 유지 (테마 시스템 보존)
- DS 리스킨(API 호환): 글래스 카드/버튼, Pretendard 타이포, 소프트 LED
- 신규: GradientWave(canvas 스펙트럼 음파), StatRing(컬러 링 타일)
- 보더리스: frame:false + 커스텀 TitleBar, 네이티브 스크롤바 제거(폭 불변)
  + OverlayScrollbars 오버레이 스크롤
- AppLayout 와이드 사이드바(lucide, 그라디언트 액티브 필), 대시보드 재구축,
  StatusBar(브랜드/로컬시간), Pretendard 번들, i18n +20키(ko/en)
This commit is contained in:
Yun Chan 2026-07-21 20:14:15 +09:00
parent f3ca0eafd4
commit a8a1d6e546
25 changed files with 1638 additions and 960 deletions

View file

@ -1,16 +1,20 @@
// src/renderer/components/AppLayout.tsx
// 시안 A+B 융합: 인스트루먼트 섀시 사이드바 + 콘텐츠 영역
// v2 "Midnight Glass": 보더리스 셸 — 와이드 사이드바(그라디언트 액티브 필) +
// 앰비언트 배경 + 오버레이 스크롤(레이아웃 폭 불변) + 커스텀 타이틀바
import { useState, useEffect } from 'react'
import { Alert, Box, Snackbar, Typography, Tooltip } from '@mui/material'
import DashboardIcon from '@mui/icons-material/Dashboard'
import HistoryIcon from '@mui/icons-material/History'
import MenuBookIcon from '@mui/icons-material/MenuBook'
import ExtensionIcon from '@mui/icons-material/Extension'
import RecordVoiceOverIcon from '@mui/icons-material/RecordVoiceOver'
import AutoStoriesIcon from '@mui/icons-material/AutoStories'
import GroupsIcon from '@mui/icons-material/Groups'
import SettingsIcon from '@mui/icons-material/Settings'
import { Alert, Box, Snackbar, Typography } from '@mui/material'
import {
LayoutDashboard,
History as HistoryIcon,
BookOpen,
SquareTerminal,
MessagesSquare,
Library,
Users,
Settings,
} from 'lucide-react'
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'
import { Led } from '@d3ro/ui/components/ds'
import { DashboardPage } from '../pages/DashboardPage'
import { HistoryPage } from '../pages/HistoryPage'
@ -23,7 +27,8 @@ import { SettingsModal } from './SettingsModal'
import { LicenseModal } from './LicenseModal'
import { OnboardingModal } from './OnboardingModal'
import { StatusBar } from './StatusBar'
import { d3roPalette, d3roFontMono, d3roTypo, d3roShadow, d3roRadius } from '@d3ro/ui/theme'
import { TitleBar } from './TitleBar'
import { d3roPalette, d3roFontSans, d3roTypo, d3roRadius, d3roShadow } from '@d3ro/ui/theme'
import { useI18n } from '@d3ro/i18n'
import type { TranslationKey } from '@d3ro/i18n'
import type { LicenseTier } from '@d3ro/core/types'
@ -33,18 +38,19 @@ type Route = 'dashboard' | 'history' | 'dictionary' | 'commands' | 'conversation
interface NavItem {
route: Route
labelKey: TranslationKey
abbr: string
icon: React.ReactElement
}
const ICON_SIZE = 18
const NAV_ITEMS: NavItem[] = [
{ route: 'dashboard', labelKey: 'nav.dashboard', abbr: 'DASH', icon: <DashboardIcon sx={{ fontSize: 20 }} /> },
{ route: 'history', labelKey: 'nav.history', abbr: 'HIST', icon: <HistoryIcon sx={{ fontSize: 20 }} /> },
{ route: 'dictionary', labelKey: 'nav.dictionary', abbr: 'DICT', icon: <MenuBookIcon sx={{ fontSize: 20 }} /> },
{ route: 'commands', labelKey: 'nav.commands', abbr: 'CMD', icon: <ExtensionIcon sx={{ fontSize: 20 }} /> },
{ route: 'conversation', labelKey: 'nav.conversation', abbr: 'TALK', icon: <RecordVoiceOverIcon sx={{ fontSize: 20 }} /> },
{ route: 'knowledge', labelKey: 'nav.knowledge', abbr: 'RAG', icon: <AutoStoriesIcon sx={{ fontSize: 20 }} /> },
{ route: 'meeting', labelKey: 'nav.meeting', abbr: 'MTG', icon: <GroupsIcon sx={{ fontSize: 20 }} /> },
{ route: 'dashboard', labelKey: 'nav.dashboard', icon: <LayoutDashboard size={ICON_SIZE} /> },
{ route: 'history', labelKey: 'nav.history', icon: <HistoryIcon size={ICON_SIZE} /> },
{ route: 'dictionary', labelKey: 'nav.dictionary', icon: <BookOpen size={ICON_SIZE} /> },
{ route: 'commands', labelKey: 'nav.commands', icon: <SquareTerminal size={ICON_SIZE} /> },
{ route: 'conversation', labelKey: 'nav.conversation', icon: <MessagesSquare size={ICON_SIZE} /> },
{ route: 'knowledge', labelKey: 'nav.knowledge', icon: <Library size={ICON_SIZE} /> },
{ route: 'meeting', labelKey: 'nav.meeting', icon: <Users size={ICON_SIZE} /> },
]
function tierToLedColor(tier: LicenseTier): 'amber' | 'green' {
@ -91,6 +97,10 @@ export function AppLayout(): React.ReactElement {
const handleOpenLicenseModal = () => setLicenseModalOpen(true)
window.addEventListener('d3ro:open-license-modal', handleOpenLicenseModal)
// 대시보드 CTA(키 설정/시스템 설정)에서 설정 모달 열기
const handleOpenSettings = () => setSettingsOpen(true)
window.addEventListener('d3ro:open-settings', handleOpenSettings)
// Phase 3.2: Premium LLM fallback/upgrade 이벤트 구독
const unsubFallback = window.electronAPI.llm.premium.onFallback((e) => {
setFallbackMsg(e.reason)
@ -105,88 +115,108 @@ export function AppLayout(): React.ReactElement {
unsubFallback()
unsubUpgradeReq()
window.removeEventListener('d3ro:open-license-modal', handleOpenLicenseModal)
window.removeEventListener('d3ro:open-settings', handleOpenSettings)
}
}, [])
return (
<Box sx={{ display: 'flex', height: '100vh', flexDirection: 'column', bgcolor: d3roPalette.bg.app }}>
<TitleBar />
<Box sx={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
{/* ── 사이드바: 인스트루먼트 섀시 스타일 ─── */}
{/* ── 사이드바: 와이드 메뉴 (레퍼런스 패턴) ─── */}
<Box
sx={{
width: 72,
width: 168,
flexShrink: 0,
bgcolor: d3roPalette.bg.sidebar,
borderRight: `1px solid ${d3roPalette.border.subtle}`,
borderRight: `1px solid ${d3roPalette.glass.hairline}`,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
py: 2,
gap: 1,
px: 1.5,
pt: 3,
pb: 2,
gap: 0.5,
}}
>
{/* 로고 LED — reflects license tier */}
<Box sx={{ mb: 2, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1 }}>
<Led color={tierToLedColor(currentTier)} pulse={currentTier !== 'free'} size={10} />
{/* 로고 — 그라디언트 브랜드 텍스트 */}
<Box sx={{ px: 1.5, mb: 3, display: 'flex', flexDirection: 'column', gap: 0.25 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Led color={tierToLedColor(currentTier)} pulse={currentTier !== 'free'} size={8} />
<Typography
sx={{
fontFamily: d3roFontSans,
fontSize: '20px',
fontWeight: 800,
letterSpacing: '0.02em',
backgroundImage: d3roPalette.gradient.logo,
backgroundClip: 'text',
WebkitBackgroundClip: 'text',
color: 'transparent',
lineHeight: 1.1,
}}
>
D3RO
</Typography>
</Box>
<Typography
sx={{
fontSize: d3roTypo.micro.size,
fontFamily: d3roFontMono,
letterSpacing: d3roTypo.micro.spacing,
fontFamily: d3roFontSans,
fontSize: '13px',
fontWeight: 500,
letterSpacing: '0.12em',
color: d3roPalette.text.dimLabel,
fontWeight: d3roTypo.micro.weight,
pl: '16px',
}}
>
D3RO
Voice
</Typography>
</Box>
{/* 네비게이션 버튼 */}
{/* 네비게이션 */}
{NAV_ITEMS.map((item) => {
const isActive = currentRoute === item.route
return (
<Tooltip key={item.route} title={t(item.labelKey)} placement="right" arrow>
<Box
onClick={() => setCurrentRoute(item.route)}
<Box
key={item.route}
onClick={() => setCurrentRoute(item.route)}
sx={{
display: 'flex',
alignItems: 'center',
gap: 1.25,
px: 1.5,
py: 1.25,
borderRadius: d3roRadius.inner,
cursor: 'pointer',
color: isActive ? d3roPalette.text.primary : d3roPalette.text.inactive,
// 액티브: 글래스 필 + 그라디언트 헤어라인 보더 + 소프트 글로우
border: '1px solid transparent',
background: isActive
? `linear-gradient(${d3roPalette.glass.raised}, ${d3roPalette.glass.raised}) padding-box, ${d3roPalette.gradient.accent} border-box`
: 'transparent',
boxShadow: isActive ? d3roShadow.glowSoft : 'none',
transition: 'color 0.15s ease, background-color 0.15s ease',
'&:hover': {
color: d3roPalette.text.primary,
bgcolor: isActive ? undefined : d3roPalette.glass.raised,
},
'& svg': {
color: isActive ? d3roPalette.accent.light : 'inherit',
flexShrink: 0,
},
}}
>
{item.icon}
<Typography
sx={{
width: 48,
height: 48,
borderRadius: d3roRadius.button,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 0.5,
cursor: 'pointer',
bgcolor: isActive ? d3roPalette.bg.chassis : 'transparent',
boxShadow: isActive
? d3roShadow.buttonPressed
: d3roShadow.buttonRaised,
color: isActive ? d3roPalette.accent.amber : d3roPalette.text.inactive,
transition: 'all 0.05s linear',
transform: isActive ? 'translateY(1px)' : 'none',
'&:active': {
transform: 'translateY(2px)',
boxShadow: d3roShadow.buttonPressed,
},
'&:hover': {
color: isActive ? d3roPalette.accent.amber : d3roPalette.text.hover,
},
fontFamily: d3roFontSans,
fontSize: d3roTypo.compact.size,
fontWeight: isActive ? 600 : 500,
lineHeight: 1,
}}
>
{item.icon}
<Typography
sx={{
fontSize: d3roTypo.nano.size,
fontFamily: d3roFontMono,
fontWeight: d3roTypo.nano.weight,
letterSpacing: d3roTypo.nano.spacing,
}}
>
{item.abbr}
</Typography>
</Box>
</Tooltip>
{t(item.labelKey)}
</Typography>
</Box>
)
})}
@ -194,50 +224,64 @@ export function AppLayout(): React.ReactElement {
<Box sx={{ flex: 1 }} />
{/* Settings */}
<Tooltip title={t('nav.settings')} placement="right" arrow>
<Box
onClick={() => setSettingsOpen(true)}
sx={{
width: 48,
height: 48,
borderRadius: d3roRadius.button,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
color: d3roPalette.text.inactive,
boxShadow: d3roShadow.buttonRaised,
transition: 'all 0.05s linear',
'&:active': {
transform: 'translateY(2px)',
boxShadow: d3roShadow.buttonPressed,
},
'&:hover': { color: d3roPalette.text.hover },
}}
>
<SettingsIcon sx={{ fontSize: 20 }} />
</Box>
</Tooltip>
<Box
onClick={() => setSettingsOpen(true)}
sx={{
display: 'flex',
alignItems: 'center',
gap: 1.25,
px: 1.5,
py: 1.25,
borderRadius: d3roRadius.inner,
cursor: 'pointer',
color: d3roPalette.text.inactive,
transition: 'color 0.15s ease, background-color 0.15s ease',
'&:hover': { color: d3roPalette.text.primary, bgcolor: d3roPalette.glass.raised },
}}
>
<Settings size={ICON_SIZE} />
<Typography sx={{ fontFamily: d3roFontSans, fontSize: d3roTypo.compact.size, fontWeight: 500, lineHeight: 1 }}>
{t('nav.settings')}
</Typography>
</Box>
</Box>
{/* ── 콘텐츠 영역 ────────────────────────── */}
{/* ── 콘텐츠 영역: 앰비언트 배경 + 오버레이 스크롤 ── */}
<Box
component="main"
sx={{
flexGrow: 1,
overflow: 'auto',
minWidth: 0,
position: 'relative',
bgcolor: d3roPalette.bg.app,
// 미묘한 방사형 비네팅 (시안 A 배경)
background: `radial-gradient(circle at 50% 30%, ${d3roPalette.bg.chassis} 0%, ${d3roPalette.bg.app} 70%)`,
// 앰비언트 그라디언트 오브 (좌상단 액센트 / 우하단 퍼플 힌트)
background: `
radial-gradient(ellipse 55% 40% at 12% -5%, ${d3roPalette.accent.dim} 0%, transparent 60%),
radial-gradient(ellipse 50% 35% at 95% 105%, ${d3roPalette.tag.purpleBg} 0%, transparent 60%),
${d3roPalette.bg.app}
`,
}}
>
{currentRoute === 'dashboard' && <DashboardPage />}
{currentRoute === 'history' && <HistoryPage />}
{currentRoute === 'dictionary' && <DictionaryPage />}
{currentRoute === 'commands' && <CommandsPage />}
{currentRoute === 'conversation' && <VoiceConversationPage />}
{currentRoute === 'knowledge' && <KnowledgeBasePage />}
{currentRoute === 'meeting' && <MeetingModePage />}
<OverlayScrollbarsComponent
defer
options={{
scrollbars: {
theme: 'os-theme-dark',
autoHide: 'move',
autoHideDelay: 800,
clickScroll: true,
},
}}
style={{ height: '100%', width: '100%' }}
>
{currentRoute === 'dashboard' && <DashboardPage />}
{currentRoute === 'history' && <HistoryPage />}
{currentRoute === 'dictionary' && <DictionaryPage />}
{currentRoute === 'commands' && <CommandsPage />}
{currentRoute === 'conversation' && <VoiceConversationPage />}
{currentRoute === 'knowledge' && <KnowledgeBasePage />}
{currentRoute === 'meeting' && <MeetingModePage />}
</OverlayScrollbarsComponent>
</Box>
</Box>
<StatusBar />