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:
parent
f3ca0eafd4
commit
a8a1d6e546
25 changed files with 1638 additions and 960 deletions
|
|
@ -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 />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue