From 082ed2ef0237ca99a22291a0558425de250c6f34 Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Tue, 21 Jul 2026 20:20:32 +0900 Subject: [PATCH] =?UTF-8?q?feat(ui):=20Midnight=20Glass=20v2=20=EB=A7=88?= =?UTF-8?q?=EA=B0=90=20=E2=80=94=20lucide=20=EC=95=84=EC=9D=B4=EC=BD=98=20?= =?UTF-8?q?=ED=86=B5=EC=9D=BC=20+=20=EB=AA=B0=EC=9E=85=20=ED=8C=A8?= =?UTF-8?q?=EB=84=90=20=EC=9B=A8=EC=9D=B4=EB=B8=8C=20+=20=ED=8C=9D?= =?UTF-8?q?=EC=97=85=20=EC=A0=95=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - @mui/icons-material → lucide-react 전환 (23파일, 잔여 0) - VoiceRecordingPanel: 9바 DOM 웨이브 → GradientWave (canvas) - Vanilla 팝업 5종 style.css v2 정합 (글래스 네이비/헤어라인/블루, CSS 변수 주입 구조 유지 — 테마 반응형 보존) --- .../renderer/components/CloudSyncSection.tsx | 13 +-- .../src/renderer/components/FileDropZone.tsx | 10 +- .../src/renderer/components/LicenseModal.tsx | 10 +- .../src/renderer/components/LicenseTab.tsx | 7 +- .../renderer/components/OllamaGuideModal.tsx | 10 +- .../renderer/components/OnboardingModal.tsx | 10 +- .../src/renderer/components/ProBadge.tsx | 4 +- .../src/renderer/components/SettingsModal.tsx | 23 ++--- .../renderer/components/TemplateSection.tsx | 17 ++-- .../components/UpgradePromptModal.tsx | 9 +- .../components/meeting/DocumentTab.tsx | 4 +- .../components/meeting/ExportMenu.tsx | 4 +- .../components/meeting/MeetingChatPanel.tsx | 13 +-- .../components/meeting/MeetingDetailTabs.tsx | 13 +-- .../components/meeting/TranscriptTab.tsx | 10 +- .../components/shared/HistoryEntryCard.tsx | 21 ++-- .../components/shared/SearchInput.tsx | 4 +- .../VoiceRecordingPanel.tsx | 98 ++++++------------- .../src/renderer/pages/CommandsPage.tsx | 29 +++--- .../src/renderer/pages/DictionaryPage.tsx | 10 +- .../src/renderer/pages/HistoryPage.tsx | 4 +- .../src/renderer/pages/KnowledgeBasePage.tsx | 19 ++-- .../src/renderer/pages/MeetingModePage.tsx | 10 +- .../renderer/pages/VoiceConversationPage.tsx | 18 ++-- .../renderer/popups/caption-overlay/style.css | 20 ++-- .../renderer/popups/command-popup/style.css | 27 ++--- .../renderer/popups/history-popup/style.css | 21 ++-- .../renderer/popups/recording-tip/style.css | 31 ++++-- .../renderer/popups/result-popup/style.css | 21 ++-- memory/project_status.md | 11 ++- 30 files changed, 212 insertions(+), 289 deletions(-) diff --git a/apps/desktop/src/renderer/components/CloudSyncSection.tsx b/apps/desktop/src/renderer/components/CloudSyncSection.tsx index 81f5548..6344b61 100644 --- a/apps/desktop/src/renderer/components/CloudSyncSection.tsx +++ b/apps/desktop/src/renderer/components/CloudSyncSection.tsx @@ -4,10 +4,7 @@ import { useEffect, useState } from 'react' import { Box, Button, Stack, Alert, CircularProgress } from '@mui/material' -import CloudIcon from '@mui/icons-material/Cloud' -import CloudDoneIcon from '@mui/icons-material/CloudDone' -import GoogleIcon from '@mui/icons-material/Google' -import GitHubIcon from '@mui/icons-material/GitHub' +import { Cloud, CloudCheck, Globe, GitBranch } from 'lucide-react' import { d3roPalette, typoSx } from '@d3ro/ui/theme' import { useI18n } from '@d3ro/i18n' @@ -128,9 +125,9 @@ export function CloudSyncSection(): React.ReactElement { {state.authenticated ? ( - + ) : ( - + )} Cloud Sync @@ -141,7 +138,7 @@ export function CloudSyncSection(): React.ReactElement { diff --git a/apps/desktop/src/renderer/components/UpgradePromptModal.tsx b/apps/desktop/src/renderer/components/UpgradePromptModal.tsx index c638cf3..0fd811f 100644 --- a/apps/desktop/src/renderer/components/UpgradePromptModal.tsx +++ b/apps/desktop/src/renderer/components/UpgradePromptModal.tsx @@ -12,8 +12,7 @@ import { Box, LinearProgress, } from '@mui/material' -import LockIcon from '@mui/icons-material/Lock' -import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline' +import { Lock, CheckCircle2 } from 'lucide-react' import { d3roPalette, d3roFontMono, d3roTypo, d3roRadius, d3roShadow } from '@d3ro/ui/theme' import { useI18n } from '@d3ro/i18n' import type { UpgradePromptEvent, UsageQuota } from '@d3ro/core/types' @@ -80,7 +79,7 @@ export function UpgradePromptModal(): React.ReactElement { color: d3roPalette.accent.amber, }} > - + {isQuota ? t('license.quotaExceeded.title', { feature: featureLabel }) : isLoginRequired @@ -134,9 +133,7 @@ export function UpgradePromptModal(): React.ReactElement { key={benefit} sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mb: 0.5 }} > - + - + {t('common.delete')} diff --git a/apps/desktop/src/renderer/components/meeting/ExportMenu.tsx b/apps/desktop/src/renderer/components/meeting/ExportMenu.tsx index ca8836d..664e9b9 100644 --- a/apps/desktop/src/renderer/components/meeting/ExportMenu.tsx +++ b/apps/desktop/src/renderer/components/meeting/ExportMenu.tsx @@ -3,7 +3,7 @@ import { useState, useCallback } from 'react' import { Menu, MenuItem } from '@mui/material' -import FileDownloadIcon from '@mui/icons-material/FileDownload' +import { Download } from 'lucide-react' import { PhysicalButton } from '@d3ro/ui/components/ds' import { d3roPalette, d3roTypo, d3roFontMono, d3roRadius, d3roShadow } from '@d3ro/ui/theme' import { useI18n } from '@d3ro/i18n' @@ -54,7 +54,7 @@ export function ExportMenu({ onExport, onCopyToClipboard }: ExportMenuProps): Re onClick={handleOpen} sx={{ height: 32, fontSize: d3roTypo.engrave.size }} > - + {t('meeting.exportFormat')} diff --git a/apps/desktop/src/renderer/components/meeting/MeetingChatPanel.tsx b/apps/desktop/src/renderer/components/meeting/MeetingChatPanel.tsx index fb6ccb6..afce403 100644 --- a/apps/desktop/src/renderer/components/meeting/MeetingChatPanel.tsx +++ b/apps/desktop/src/renderer/components/meeting/MeetingChatPanel.tsx @@ -3,10 +3,7 @@ import { useState, useCallback, useEffect, useRef, useMemo } from 'react' import { Box, TextField, IconButton, Tooltip } from '@mui/material' -import SendIcon from '@mui/icons-material/Send' -import DeleteSweepIcon from '@mui/icons-material/DeleteSweep' -import ExpandLessIcon from '@mui/icons-material/ExpandLess' -import ExpandMoreIcon from '@mui/icons-material/ExpandMore' +import { Send, Trash2, ChevronUp, ChevronDown } from 'lucide-react' import { PhosphorText } from '@d3ro/ui/components/ds' import { PhysicalButton } from '@d3ro/ui/components/ds' import { isImeComposingEvent } from '../../utils/keyboard' @@ -187,16 +184,16 @@ export function MeetingChatPanel({ sessionId }: MeetingChatPanelProps): React.Re onClick={handleClear} disabled={messages.length === 0 && !streaming} > - + {collapsed ? ( - + ) : ( - + )} @@ -408,7 +405,7 @@ export function MeetingChatPanel({ sessionId }: MeetingChatPanelProps): React.Re disabled={!inputValue.trim() || streaming} sx={{ flexShrink: 0, height: 36, minWidth: 36, px: 1 }} > - + diff --git a/apps/desktop/src/renderer/components/meeting/MeetingDetailTabs.tsx b/apps/desktop/src/renderer/components/meeting/MeetingDetailTabs.tsx index dc9cf35..0cd3987 100644 --- a/apps/desktop/src/renderer/components/meeting/MeetingDetailTabs.tsx +++ b/apps/desktop/src/renderer/components/meeting/MeetingDetailTabs.tsx @@ -10,10 +10,7 @@ import { TextField, Tooltip, } from '@mui/material' -import ArrowBackIcon from '@mui/icons-material/ArrowBack' -import AddIcon from '@mui/icons-material/Add' -import EditIcon from '@mui/icons-material/Edit' -import CheckIcon from '@mui/icons-material/Check' +import { ArrowLeft, Plus, Pencil, Check } from 'lucide-react' import { TranscriptTab } from './TranscriptTab' import { DocumentTab } from './DocumentTab' import { AddDocumentDialog } from './AddDocumentDialog' @@ -231,7 +228,7 @@ export function MeetingDetailTabs({ }} > - + {editingTitle ? ( @@ -251,7 +248,7 @@ export function MeetingDetailTabs({ autoFocus /> - + ) : ( @@ -264,7 +261,7 @@ export function MeetingDetailTabs({ size="small" onClick={() => { setEditingTitle(true); setTitleDraft(detail.title ?? '') }} > - + @@ -303,7 +300,7 @@ export function MeetingDetailTabs({ ))} } + icon={} label={t('meeting.addDocument')} iconPosition="start" sx={{ minWidth: 120 }} diff --git a/apps/desktop/src/renderer/components/meeting/TranscriptTab.tsx b/apps/desktop/src/renderer/components/meeting/TranscriptTab.tsx index 591b27e..1537437 100644 --- a/apps/desktop/src/renderer/components/meeting/TranscriptTab.tsx +++ b/apps/desktop/src/renderer/components/meeting/TranscriptTab.tsx @@ -8,9 +8,7 @@ import { PhysicalButton } from '@d3ro/ui/components/ds' import { PhosphorText } from '@d3ro/ui/components/ds' import { d3roPalette, d3roFontMono, d3roTypo } from '@d3ro/ui/theme' import { useI18n } from '@d3ro/i18n' -import FileDownloadIcon from '@mui/icons-material/FileDownload' -import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh' -import RecordVoiceOverIcon from '@mui/icons-material/RecordVoiceOver' +import { Download, WandSparkles, Speech } from 'lucide-react' interface TranscriptTabProps { sessionId: string @@ -129,7 +127,7 @@ export function TranscriptTab({ disabled={polishing || diarizing} sx={{ height: 30, fontSize: d3roTypo.engrave.size }} > - + {polishing ? t('meeting.polishing') : t('meeting.polish')} {onDiarize && ( @@ -139,7 +137,7 @@ export function TranscriptTab({ disabled={polishing || diarizing} sx={{ height: 30, fontSize: d3roTypo.engrave.size }} > - + {diarizing ? t('meeting.diarizing') : t('meeting.diarize')} )} @@ -148,7 +146,7 @@ export function TranscriptTab({ onClick={handleDownloadTxt} sx={{ height: 30, fontSize: d3roTypo.engrave.size }} > - + {t('meeting.downloadTranscript')} diff --git a/apps/desktop/src/renderer/components/shared/HistoryEntryCard.tsx b/apps/desktop/src/renderer/components/shared/HistoryEntryCard.tsx index ddf276b..00e96c0 100644 --- a/apps/desktop/src/renderer/components/shared/HistoryEntryCard.tsx +++ b/apps/desktop/src/renderer/components/shared/HistoryEntryCard.tsx @@ -4,12 +4,7 @@ import { useState, useEffect, useCallback } from 'react' import { Box, IconButton, Tooltip, Chip } from '@mui/material' -import ContentCopyIcon from '@mui/icons-material/ContentCopy' -import DeleteIcon from '@mui/icons-material/Delete' -import LocalOfferIcon from '@mui/icons-material/LocalOffer' -import CloseIcon from '@mui/icons-material/Close' -import SummarizeIcon from '@mui/icons-material/Summarize' -import ExpandMoreIcon from '@mui/icons-material/ExpandMore' +import { Copy, Trash2, Tag, X, ScrollText, ChevronDown } from 'lucide-react' import { MetalCard, Led } from '@d3ro/ui/components/ds' import { d3roPalette, d3roFontMono, d3roTypo, d3roRadius } from '@d3ro/ui/theme' import { useI18n } from '@d3ro/i18n' @@ -149,7 +144,7 @@ export function HistoryEntryCard({ entry, onCopy, onDelete, showTags = false, on {entry.mode.toUpperCase()} {hasSummary && ( } + icon={} label={t('meetingSummary.title')} size="small" onClick={handleToggleSummary} @@ -176,7 +171,7 @@ export function HistoryEntryCard({ entry, onCopy, onDelete, showTags = false, on size="small" onClick={() => onTagClick?.(tag.tag)} onDelete={() => handleRemoveTag(tag.tag)} - deleteIcon={} + deleteIcon={} sx={{ height: 20, fontFamily: d3roFontMono, @@ -219,7 +214,7 @@ export function HistoryEntryCard({ entry, onCopy, onDelete, showTags = false, on onClick={() => setShowTagInput(true)} sx={{ p: 0.25, color: d3roPalette.text.muted, '&:hover': { color: d3roPalette.accent.amber } }} > - + )} @@ -234,7 +229,7 @@ export function HistoryEntryCard({ entry, onCopy, onDelete, showTags = false, on onClick={() => onCopy(displayText)} sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.amber } }} > - + )} @@ -245,7 +240,7 @@ export function HistoryEntryCard({ entry, onCopy, onDelete, showTags = false, on onClick={() => onDelete(entry.id)} sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.tag.red } }} > - + )} @@ -290,7 +285,7 @@ export function HistoryEntryCard({ entry, onCopy, onDelete, showTags = false, on - + @@ -324,7 +319,7 @@ export function HistoryEntryCard({ entry, onCopy, onDelete, showTags = false, on sx={{ mt: 1, textAlign: 'center', cursor: 'pointer', color: d3roPalette.text.muted, '&:hover': { color: d3roPalette.accent.amber } }} onClick={handleToggleSummary} > - + )} diff --git a/apps/desktop/src/renderer/components/shared/SearchInput.tsx b/apps/desktop/src/renderer/components/shared/SearchInput.tsx index 462860a..77dd892 100644 --- a/apps/desktop/src/renderer/components/shared/SearchInput.tsx +++ b/apps/desktop/src/renderer/components/shared/SearchInput.tsx @@ -2,7 +2,7 @@ // 공유: 모노 폰트 검색 입력 필드 import { TextField, InputAdornment } from '@mui/material' -import SearchIcon from '@mui/icons-material/Search' +import { Search } from 'lucide-react' import { d3roPalette, d3roFontMono, d3roTypo } from '@d3ro/ui/theme' interface SearchInputProps { @@ -30,7 +30,7 @@ export function SearchInput({ value, onChange, placeholder }: SearchInputProps): input: { startAdornment: ( - + ), }, diff --git a/apps/desktop/src/renderer/components/voice-conversation/VoiceRecordingPanel.tsx b/apps/desktop/src/renderer/components/voice-conversation/VoiceRecordingPanel.tsx index afbcc59..111ce00 100644 --- a/apps/desktop/src/renderer/components/voice-conversation/VoiceRecordingPanel.tsx +++ b/apps/desktop/src/renderer/components/voice-conversation/VoiceRecordingPanel.tsx @@ -1,34 +1,20 @@ // src/renderer/components/voice-conversation/VoiceRecordingPanel.tsx -// Phase 5 Part 6: Voice Conversation 몰입 녹음 패널 -// recording-tip 팝업의 9바 waveform (cos 분포, 100ms, smoothing)을 React로 포팅. -// 기존 구현: src/renderer/popups/recording-tip/script.js (Vanilla JS, 팝업 전용) -// 스펙: docs/design/03-db-and-ui.md:428+ +// Voice Conversation 몰입 녹음 패널 (리디자인 v2) +// 기존 9바 DOM waveform을 DS의 GradientWave(canvas 스펙트럼 웨이브)로 교체. // // AudioLevelSource: // - VoiceConversationService가 listening 상태에서만 AUDIO_LEVEL 채널로 forwarding. // - level 값은 0.0~1.0. 구독자가 없는 thinking/speaking에는 Panel 자체가 언마운트되므로 // useEffect cleanup으로 subscription이 자동 해제됨. +// - 레벨 스무딩/유휴 진동은 GradientWave 내부에서 처리하므로 여기서는 raw level만 전달. import { useEffect, useRef, useState } from 'react' import { Box } from '@mui/material' -import { Led, PhosphorText } from '@d3ro/ui/components/ds' +import { GradientWave, Led, PhosphorText } from '@d3ro/ui/components/ds' import { d3roPalette, d3roFontMono } from '@d3ro/ui/theme' import { useI18n } from '@d3ro/i18n' -// ── Waveform 상수 (recording-tip/script.js 수치 그대로) ── -const BAR_COUNT = 9 -const UPDATE_INTERVAL = 100 -const MIN_HEIGHT = 2 -const MAX_HEIGHT = 28 -const SMOOTHING = 0.5 -const RANDOM_FACTOR = 0.35 - -/** 코사인 분포 가중치 (중앙이 가장 높음). 설계서 03. */ -const weights: readonly number[] = Array.from({ length: BAR_COUNT }, (_, i) => { - const center = (BAR_COUNT - 1) / 2 - const normalized = (i - center) / center - return Math.cos((normalized * Math.PI) / 2) -}) +const WAVE_HEIGHT = 48 function formatDuration(ms: number): string { const elapsed = Math.floor(ms / 1000) @@ -39,59 +25,34 @@ function formatDuration(ms: number): string { /** * Voice Conversation이 listening 상태일 때만 렌더되는 몰입 녹음 패널. - * - 9바 phosphor waveform (audio-level 기반) + * - GradientWave 스펙트럼 웨이브 (audio-level 기반, 유휴 애니메이션 내장) * - REC LED + elapsed 타이머 * - "SPEAK NOW · PRESS ⏹ TO SEND" 힌트 (i18n) */ export function VoiceRecordingPanel(): React.ReactElement { const { t } = useI18n() - const [heights, setHeights] = useState(() => - new Array(BAR_COUNT).fill(MIN_HEIGHT), - ) + const [audioLevel, setAudioLevel] = useState(0) const [elapsedMs, setElapsedMs] = useState(0) - - const heightsRef = useRef(new Array(BAR_COUNT).fill(MIN_HEIGHT)) - const audioLevelRef = useRef(0) const startedAtRef = useRef(Date.now()) // audio-level 구독 (VoiceConversationService → AUDIO_LEVEL) useEffect(() => { const unsubscribe = window.electronAPI.voiceConversation.onAudioLevel((e) => { - audioLevelRef.current = e.level + setAudioLevel(e.level) }) return unsubscribe }, []) - // 9바 애니메이션 루프 (100ms interval) + // 경과 시간 타이머 useEffect(() => { startedAtRef.current = Date.now() - heightsRef.current = new Array(BAR_COUNT).fill(MIN_HEIGHT) - audioLevelRef.current = 0 - - const animInterval = window.setInterval(() => { - const level = audioLevelRef.current - // 최소 진동: audioLevel이 0이어도 바가 미세하게 움직여 "살아있음" 표현 - const effectiveLevel = Math.max(0.08, level) - const next = new Array(BAR_COUNT).fill(MIN_HEIGHT) as number[] - for (let i = 0; i < BAR_COUNT; i++) { - const baseTarget = effectiveLevel * MAX_HEIGHT * weights[i] - const randomized = baseTarget * (1 + (Math.random() - 0.5) * 2 * RANDOM_FACTOR) - const target = Math.max(MIN_HEIGHT, Math.min(MAX_HEIGHT, randomized)) - // 스무딩 보간 - const prev = heightsRef.current[i] - const smoothed = prev + (target - prev) * SMOOTHING - heightsRef.current[i] = smoothed - next[i] = Math.round(smoothed) - } - setHeights(next) - }, UPDATE_INTERVAL) + setElapsedMs(0) const durationInterval = window.setInterval(() => { setElapsedMs(Date.now() - startedAtRef.current) }, 500) return () => { - window.clearInterval(animInterval) window.clearInterval(durationInterval) } }, []) @@ -111,7 +72,7 @@ export function VoiceRecordingPanel(): React.ReactElement { {/* REC LED + 타이머 */} - + {t('conversation.state.recording')} - {/* 9바 waveform */} + {/* 그라디언트 스펙트럼 웨이브 */} - {heights.map((h, i) => ( - - ))} + {/* 힌트 */} diff --git a/apps/desktop/src/renderer/pages/CommandsPage.tsx b/apps/desktop/src/renderer/pages/CommandsPage.tsx index 82b15f9..5699752 100644 --- a/apps/desktop/src/renderer/pages/CommandsPage.tsx +++ b/apps/desktop/src/renderer/pages/CommandsPage.tsx @@ -4,12 +4,7 @@ import { useState, useEffect, useCallback } from 'react' import { Box, Button, IconButton, Dialog, DialogTitle, DialogContent, DialogActions, TextField, Chip, Select, MenuItem, FormControl, InputLabel } from '@mui/material' -import AddIcon from '@mui/icons-material/Add' -import DeleteIcon from '@mui/icons-material/Delete' -import EditIcon from '@mui/icons-material/Edit' -import CheckCircleIcon from '@mui/icons-material/CheckCircle' -import PlayArrowIcon from '@mui/icons-material/PlayArrow' -import LinkIcon from '@mui/icons-material/Link' +import { Plus, Trash2, Pencil, CircleCheck, Play, Link } from 'lucide-react' import { MetalCard, PhosphorText, Led, ScreenPanel } from '@d3ro/ui/components/ds' import { PageHeader, EmptyStateCard } from '../components/shared' import { isImeComposingEvent } from '../utils/keyboard' @@ -131,7 +126,7 @@ export function CommandsPage(): React.ReactElement { title={t('commands.title').toUpperCase()} count={t('commands.count', { count: instructions.length }).toUpperCase()} action={ - } @@ -183,7 +178,7 @@ export function CommandsPage(): React.ReactElement { {isActive ? ( - + ) : ( )} @@ -210,7 +205,7 @@ export function CommandsPage(): React.ReactElement { onClick={() => openEdit(inst)} sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.amber } }} > - + {!inst.isBuiltin && ( handleDelete(inst.id)} sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.tag.red } }} > - + )} @@ -360,7 +355,7 @@ function VoiceKeywordsSection({ instructions }: { instructions: CustomInstructio onClick={() => { setEditingRule(rule.instructionId); setKeywordInput('') }} sx={{ p: 0.25, color: d3roPalette.text.muted, '&:hover': { color: d3roPalette.accent.amber } }} > - + )} @@ -456,7 +451,7 @@ function ChainSection({ instructions }: { instructions: CustomInstruction[] }): {t('chain.title').toUpperCase()} - @@ -497,15 +492,15 @@ function ChainSection({ instructions }: { instructions: CustomInstruction[] }): handleExecute(chain.id)} sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.tag.green } }}> - + openEdit(chain)} sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.amber } }}> - + handleDelete(chain.id)} sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.tag.red } }}> - + @@ -563,12 +558,12 @@ function ChainSection({ instructions }: { instructions: CustomInstruction[] }): )} removeStep(idx)} disabled={formSteps.length <= 1} sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.tag.red } }}> - + ))} - diff --git a/apps/desktop/src/renderer/pages/DictionaryPage.tsx b/apps/desktop/src/renderer/pages/DictionaryPage.tsx index d0b8b4c..936fee7 100644 --- a/apps/desktop/src/renderer/pages/DictionaryPage.tsx +++ b/apps/desktop/src/renderer/pages/DictionaryPage.tsx @@ -3,9 +3,7 @@ import { useState, useEffect, useCallback } from 'react' import { Box, TextField, Button, IconButton, Dialog, DialogTitle, DialogContent, DialogActions } from '@mui/material' -import AddIcon from '@mui/icons-material/Add' -import DeleteIcon from '@mui/icons-material/Delete' -import EditIcon from '@mui/icons-material/Edit' +import { Plus, Trash2, Pencil } from 'lucide-react' import { MetalCard, PhosphorText } from '@d3ro/ui/components/ds' import { PageHeader, SearchInput, EmptyStateCard } from '../components/shared' import { d3roPalette, d3roFontMono, d3roTypo } from '@d3ro/ui/theme' @@ -73,7 +71,7 @@ export function DictionaryPage(): React.ReactElement { title={t('dictionary.title').toUpperCase()} count={t('dictionary.words', { count: data?.total ?? 0 }).toUpperCase()} action={ - } @@ -114,11 +112,11 @@ export function DictionaryPage(): React.ReactElement { openEdit(entry)} sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.amber } }}> - + { window.electronAPI.dictionary.delete({ id: entry.id }); loadData() }} sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.tag.red } }}> - + diff --git a/apps/desktop/src/renderer/pages/HistoryPage.tsx b/apps/desktop/src/renderer/pages/HistoryPage.tsx index 9bb2563..97eb46b 100644 --- a/apps/desktop/src/renderer/pages/HistoryPage.tsx +++ b/apps/desktop/src/renderer/pages/HistoryPage.tsx @@ -4,7 +4,7 @@ import { useState, useEffect, useCallback, useMemo } from 'react' import { Box, Chip, IconButton, Tooltip } from '@mui/material' -import FileDownloadIcon from '@mui/icons-material/FileDownload' +import { Download } from 'lucide-react' import { PhosphorText } from '@d3ro/ui/components/ds' import { d3roPalette, d3roTypo, d3roFontMono, d3roRadius } from '@d3ro/ui/theme' import { useI18n } from '@d3ro/i18n' @@ -96,7 +96,7 @@ export function HistoryPage(): React.ReactElement { onClick={handleExport} sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.amber } }} > - + diff --git a/apps/desktop/src/renderer/pages/KnowledgeBasePage.tsx b/apps/desktop/src/renderer/pages/KnowledgeBasePage.tsx index 3bf904b..56c0333 100644 --- a/apps/desktop/src/renderer/pages/KnowledgeBasePage.tsx +++ b/apps/desktop/src/renderer/pages/KnowledgeBasePage.tsx @@ -4,12 +4,7 @@ import { useState, useEffect, useCallback } from 'react' import { Box, TextField, IconButton, Tooltip, LinearProgress } from '@mui/material' -import AddIcon from '@mui/icons-material/Add' -import DeleteIcon from '@mui/icons-material/Delete' -import RefreshIcon from '@mui/icons-material/Refresh' -import SearchIcon from '@mui/icons-material/Search' -import SendIcon from '@mui/icons-material/Send' -import DescriptionIcon from '@mui/icons-material/Description' +import { Plus, Trash2, RefreshCw, Search, Send, FileText } from 'lucide-react' import { MetalCard, PhosphorText, Led, PhysicalButton, ScreenPanel } from '@d3ro/ui/components/ds' import { PageHeader, EmptyStateCard } from '../components/shared' import { isImeComposingEvent } from '../utils/keyboard' @@ -96,7 +91,7 @@ export function KnowledgeBasePage(): React.ReactElement { title={t('rag.title').toUpperCase()} action={ - {adding ? t('rag.adding') : t('rag.addDocument')} + {adding ? t('rag.adding') : t('rag.addDocument')} } /> @@ -160,7 +155,7 @@ export function KnowledgeBasePage(): React.ReactElement { {documents.map((doc) => ( - + {doc.fileName} @@ -176,13 +171,13 @@ export function KnowledgeBasePage(): React.ReactElement { handleReindex(doc.id)} sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.amber } }}> - + handleRemoveDocument(doc.id)} sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.tag.red } }}> - + @@ -198,7 +193,7 @@ export function KnowledgeBasePage(): React.ReactElement { - + setQuery(e.target.value)} @@ -219,7 +214,7 @@ export function KnowledgeBasePage(): React.ReactElement { /> - + diff --git a/apps/desktop/src/renderer/pages/MeetingModePage.tsx b/apps/desktop/src/renderer/pages/MeetingModePage.tsx index 7a846f0..ca88567 100644 --- a/apps/desktop/src/renderer/pages/MeetingModePage.tsx +++ b/apps/desktop/src/renderer/pages/MeetingModePage.tsx @@ -9,9 +9,7 @@ import { Snackbar, Alert, } from '@mui/material' -import AddIcon from '@mui/icons-material/Add' -import StopIcon from '@mui/icons-material/Stop' -import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord' +import { Plus, Square, Circle } from 'lucide-react' import { MetalCard, PhosphorText, Led, PhysicalButton, ScreenPanel } from '@d3ro/ui/components/ds' import { MeetingDetailTabs } from '../components/meeting/MeetingDetailTabs' import { EditableSegment } from '../components/meeting/EditableSegment' @@ -229,7 +227,7 @@ export function MeetingModePage(): React.ReactElement { count={totalSessions > 0 ? t('meeting.sessions', { count: totalSessions }) : undefined} action={ - + {t('meeting.newMeeting')} } @@ -344,7 +342,7 @@ export function MeetingModePage(): React.ReactElement { {/* 상단 바 */} - + {formatTime(elapsedMs)} @@ -354,7 +352,7 @@ export function MeetingModePage(): React.ReactElement { {!isProcessing && ( - + {t('meeting.stopRecording')} )} diff --git a/apps/desktop/src/renderer/pages/VoiceConversationPage.tsx b/apps/desktop/src/renderer/pages/VoiceConversationPage.tsx index 7eb4ded..1dd28da 100644 --- a/apps/desktop/src/renderer/pages/VoiceConversationPage.tsx +++ b/apps/desktop/src/renderer/pages/VoiceConversationPage.tsx @@ -4,11 +4,7 @@ import { useState, useEffect, useCallback, useRef } from 'react' import { Alert, Box, IconButton, Snackbar, TextField, Tooltip } from '@mui/material' -import MicIcon from '@mui/icons-material/Mic' -import StopIcon from '@mui/icons-material/Stop' -import SendIcon from '@mui/icons-material/Send' -import DeleteSweepIcon from '@mui/icons-material/DeleteSweep' -import CancelIcon from '@mui/icons-material/Cancel' +import { Mic, Square, Send, Trash2, XCircle } from 'lucide-react' import { MetalCard, PhosphorText, Led, PhysicalButton, ScreenPanel, InstrumentPanel } from '@d3ro/ui/components/ds' import { PageHeader } from '../components/shared' import { VoiceRecordingPanel } from '../components/voice-conversation/VoiceRecordingPanel' @@ -248,7 +244,7 @@ export function VoiceConversationPage(): React.ReactElement { {messages.length > 0 && ( - + )} @@ -378,7 +374,7 @@ export function VoiceConversationPage(): React.ReactElement { {/* 녹음 버튼 */} {!isActive ? ( - + ) : state === 'listening' ? ( - + ) : state === 'thinking' || state === 'speaking' ? ( - + ) : ( - + )} @@ -439,7 +435,7 @@ export function VoiceConversationPage(): React.ReactElement { : d3roPalette.text.inactive, }} > - + )} diff --git a/apps/desktop/src/renderer/popups/caption-overlay/style.css b/apps/desktop/src/renderer/popups/caption-overlay/style.css index 81ad4b7..5184bb6 100644 --- a/apps/desktop/src/renderer/popups/caption-overlay/style.css +++ b/apps/desktop/src/renderer/popups/caption-overlay/style.css @@ -3,14 +3,14 @@ * WindowManager.insertCSS()가 :root에 테마 변수를 주입하면 자동 전환. */ -/* ── 다크 테마 fallback 기본값 (:root) ─────────────────────────── */ +/* ── Midnight(다크) fallback 기본값 (:root) ────────────────────── */ :root { - --d3-accent-main: #f25b29; - --d3-accent-glow: rgba(242, 91, 41, 0.6); - --d3-accent-glow-dim: rgba(242, 91, 41, 0.3); - --d3-accent-light: #ff8a65; - --d3-accent-light-glow: rgba(255, 138, 101, 0.7); - --d3-accent-light-glow-dim: rgba(255, 138, 101, 0.4); + --d3-accent-main: #3b82f6; + --d3-accent-glow: rgba(59, 130, 246, 0.6); + --d3-accent-glow-dim: rgba(59, 130, 246, 0.3); + --d3-accent-light: #60a5fa; + --d3-accent-light-glow: rgba(96, 165, 250, 0.7); + --d3-accent-light-glow-dim: rgba(96, 165, 250, 0.4); } * { @@ -59,8 +59,10 @@ html, body { 0 0 8px var(--d3-accent-glow), 0 0 16px var(--d3-accent-glow-dim), 0 1px 3px rgba(0, 0, 0, 0.8); - background: rgba(0, 0, 0, 0.92); - border-radius: 6px; + background: rgba(13, 20, 38, 0.92); + /* 헤어라인은 box-shadow inset — border는 라인 높이에 영향을 주므로 금지 */ + box-shadow: inset 0 0 0 1px rgba(148, 180, 255, 0.1); + border-radius: 8px; padding: 4px 12px; opacity: 1; transition: opacity 0.5s ease-out; diff --git a/apps/desktop/src/renderer/popups/command-popup/style.css b/apps/desktop/src/renderer/popups/command-popup/style.css index 6dfa83d..1e05413 100644 --- a/apps/desktop/src/renderer/popups/command-popup/style.css +++ b/apps/desktop/src/renderer/popups/command-popup/style.css @@ -3,19 +3,19 @@ * WindowManager.insertCSS()가 :root에 테마 변수를 주입하면 자동 전환. */ -/* ── 다크 테마 fallback 기본값 (:root) ─────────────────────────── */ +/* ── Midnight(다크) fallback 기본값 (:root) ────────────────────── */ :root { - --d3-bg-card: #242427; - --d3-border-default: rgba(255, 255, 255, 0.08); - --d3-border-subtle: rgba(255, 255, 255, 0.06); - --d3-text-primary: rgba(255, 255, 255, 0.87); - --d3-text-inactive: rgba(255, 255, 255, 0.3); - --d3-text-muted: rgba(255, 255, 255, 0.25); - --d3-text-dimLabel: #5c2615; - --d3-text-secondary: rgba(255, 255, 255, 0.35); - --d3-accent-main: #f25b29; - --d3-accent-dim: rgba(242, 91, 41, 0.08); - --d3-shadow-popup: 0 8px 32px rgba(0, 0, 0, 0.5); + --d3-bg-card: rgba(17, 26, 48, 0.85); + --d3-border-default: rgba(148, 180, 255, 0.12); + --d3-border-subtle: rgba(148, 180, 255, 0.07); + --d3-text-primary: #eef2fb; + --d3-text-inactive: #7385ab; + --d3-text-muted: rgba(147, 164, 200, 0.45); + --d3-text-dimLabel: #4e5f85; + --d3-text-secondary: #93a4c8; + --d3-accent-main: #3b82f6; + --d3-accent-dim: rgba(59, 130, 246, 0.14); + --d3-shadow-popup: 0 12px 40px rgba(3, 7, 18, 0.5); } * { @@ -39,8 +39,9 @@ body { .command-popup { background: var(--d3-bg-card); border: 1px solid var(--d3-border-default); - border-radius: 12px; + border-radius: 14px; padding: 6px; + backdrop-filter: blur(24px); box-shadow: var(--d3-shadow-popup); opacity: 0; transform: translateY(4px) scale(0.98); diff --git a/apps/desktop/src/renderer/popups/history-popup/style.css b/apps/desktop/src/renderer/popups/history-popup/style.css index cbc8e11..8a3d9bc 100644 --- a/apps/desktop/src/renderer/popups/history-popup/style.css +++ b/apps/desktop/src/renderer/popups/history-popup/style.css @@ -3,16 +3,16 @@ * WindowManager.insertCSS()가 :root에 테마 변수를 주입하면 자동 전환. */ -/* ── 다크 테마 fallback 기본값 (:root) ─────────────────────────── */ +/* ── Midnight(다크) fallback 기본값 (:root) ────────────────────── */ :root { - --d3-bg-card: #242427; - --d3-border-default: rgba(255, 255, 255, 0.08); - --d3-border-subtle: rgba(255, 255, 255, 0.06); - --d3-text-primary: rgba(255, 255, 255, 0.87); - --d3-text-inactive: rgba(255, 255, 255, 0.3); - --d3-text-muted: rgba(255, 255, 255, 0.25); - --d3-accent-main: #f25b29; - --d3-shadow-popup: 0 8px 32px rgba(0, 0, 0, 0.5); + --d3-bg-card: rgba(17, 26, 48, 0.85); + --d3-border-default: rgba(148, 180, 255, 0.12); + --d3-border-subtle: rgba(148, 180, 255, 0.07); + --d3-text-primary: #eef2fb; + --d3-text-inactive: #7385ab; + --d3-text-muted: rgba(147, 164, 200, 0.45); + --d3-accent-main: #3b82f6; + --d3-shadow-popup: 0 12px 40px rgba(3, 7, 18, 0.5); } * { @@ -36,8 +36,9 @@ body { .history-popup { background: var(--d3-bg-card); border: 1px solid var(--d3-border-default); - border-radius: 12px; + border-radius: 14px; padding: 6px; + backdrop-filter: blur(24px); box-shadow: var(--d3-shadow-popup); opacity: 0; transform: translateY(4px) scale(0.98); diff --git a/apps/desktop/src/renderer/popups/recording-tip/style.css b/apps/desktop/src/renderer/popups/recording-tip/style.css index daf788f..6bfd71a 100644 --- a/apps/desktop/src/renderer/popups/recording-tip/style.css +++ b/apps/desktop/src/renderer/popups/recording-tip/style.css @@ -3,13 +3,15 @@ * WindowManager.insertCSS()가 :root에 테마 변수를 주입하면 자동 전환. */ -/* ── 다크 테마 fallback 기본값 (:root) ─────────────────────────── */ +/* ── Midnight(다크) fallback 기본값 (:root) ────────────────────── */ :root { - --d3-bg-tip: rgba(0, 0, 0, 0.85); - --d3-text-primary: rgba(255, 255, 255, 0.87); - --d3-text-secondary: rgba(255, 255, 255, 0.6); - --d3-accent-main: #f25b29; - --d3-border-strong: rgba(255, 255, 255, 0.15); + --d3-bg-tip: rgba(17, 26, 48, 0.85); + --d3-text-primary: #eef2fb; + --d3-text-secondary: #93a4c8; + --d3-accent-main: #3b82f6; + --d3-border-default: rgba(148, 180, 255, 0.12); + --d3-border-strong: rgba(148, 180, 255, 0.2); + --d3-shadow-popup: 0 12px 40px rgba(3, 7, 18, 0.5); } * { @@ -35,12 +37,14 @@ body { .recording-tip { background: var(--d3-bg-tip); - border-radius: 8px; + border: 1px solid var(--d3-border-default); + border-radius: 14px; padding: 8px 12px; display: flex; align-items: center; gap: 8px; - backdrop-filter: blur(10px); + backdrop-filter: blur(24px); + box-shadow: var(--d3-shadow-popup); transition: opacity 150ms ease-in-out; cursor: pointer; } @@ -60,6 +64,17 @@ body { min-height: 2px; } +/* 9바 시안→블루→퍼플 스펙트럼 (v2 Midnight — RAW.dark gradient.wave1~3 보간) */ +.wave-bar:nth-child(1) { background: #22d3ee; box-shadow: 0 0 6px rgba(34, 211, 238, 0.35); } +.wave-bar:nth-child(2) { background: #28bef0; box-shadow: 0 0 6px rgba(40, 190, 240, 0.35); } +.wave-bar:nth-child(3) { background: #2fa9f2; box-shadow: 0 0 6px rgba(47, 169, 242, 0.35); } +.wave-bar:nth-child(4) { background: #3596f4; box-shadow: 0 0 6px rgba(53, 150, 244, 0.35); } +.wave-bar:nth-child(5) { background: #3b82f6; box-shadow: 0 0 6px rgba(59, 130, 246, 0.35); } +.wave-bar:nth-child(6) { background: #4f78f6; box-shadow: 0 0 6px rgba(79, 120, 246, 0.35); } +.wave-bar:nth-child(7) { background: #636ff6; box-shadow: 0 0 6px rgba(99, 111, 246, 0.35); } +.wave-bar:nth-child(8) { background: #7765f6; box-shadow: 0 0 6px rgba(119, 101, 246, 0.35); } +.wave-bar:nth-child(9) { background: #8b5cf6; box-shadow: 0 0 6px rgba(139, 92, 246, 0.35); } + .duration { color: var(--d3-text-primary); font-size: 13px; diff --git a/apps/desktop/src/renderer/popups/result-popup/style.css b/apps/desktop/src/renderer/popups/result-popup/style.css index 423b805..2cc38fb 100644 --- a/apps/desktop/src/renderer/popups/result-popup/style.css +++ b/apps/desktop/src/renderer/popups/result-popup/style.css @@ -4,15 +4,15 @@ * WindowManager.insertCSS()가 :root에 테마 변수를 주입하면 자동 전환. */ -/* ── 다크 테마 fallback 기본값 (:root) ─────────────────────────── */ +/* ── Midnight(다크) fallback 기본값 (:root) ────────────────────── */ :root { - --d3-bg-result: #1e1e1e; - --d3-border-result: rgba(255, 255, 255, 0.08); - --d3-text-result: rgba(255, 255, 255, 0.87); - --d3-action-btn: rgba(255, 255, 255, 0.4); - --d3-action-btn-hover-bg: rgba(255, 255, 255, 0.08); - --d3-action-btn-hover: rgba(255, 255, 255, 0.7); - --d3-shadow-popup: 0 8px 32px rgba(0, 0, 0, 0.5); + --d3-bg-result: rgba(17, 26, 48, 0.85); + --d3-border-result: rgba(148, 180, 255, 0.12); + --d3-text-result: #eef2fb; + --d3-action-btn: rgba(147, 164, 200, 0.7); + --d3-action-btn-hover-bg: rgba(148, 180, 255, 0.08); + --d3-action-btn-hover: #c9d5f2; + --d3-shadow-popup: 0 12px 40px rgba(3, 7, 18, 0.5); } * { @@ -37,8 +37,9 @@ body { .result-popup { background: var(--d3-bg-result); border: 1px solid var(--d3-border-result); - border-radius: 12px; + border-radius: 14px; padding: 12px 16px; + backdrop-filter: blur(24px); box-shadow: var(--d3-shadow-popup); opacity: 0; transform: translateY(4px); @@ -89,7 +90,7 @@ body { } .action-btn.copied { - color: #4caf50; + color: #34d399; } .hidden { diff --git a/memory/project_status.md b/memory/project_status.md index ac17d47..644418a 100644 --- a/memory/project_status.md +++ b/memory/project_status.md @@ -18,11 +18,12 @@ - **StatusBar**: 브랜드+버전+모델 / 로컬 시간 시계 - 검증: tsc 0, vitest 41/41, 실행 스크린샷으로 대시보드/히스토리/대화/회의/지식/설정 전 페이지 확인. 스크롤 시 폭 불변 확인. 상단 1px 잡티는 detached DevTools 겹침(앱 버그 아님) -### 남은 마감(진행 중) -1. 내부 페이지들의 @mui/icons-material → lucide-react 통일 -2. Vanilla 팝업 5종(recording-tip 등) 스타일 v2 정합 -3. VoiceRecordingPanel(대화 몰입 패널) → GradientWave 교체 -4. 라이트 테마 시각 검수, v0.2.0-alpha 릴리스 +### 마감 완료 (워크플로 8 에이전트 병렬) +1. ✅ @mui/icons-material → lucide-react 전환 (23파일, 잔여 0 — 의존성 제거는 추후) +2. ✅ Vanilla 팝업 5종 CSS v2 정합 (글래스 네이비 + 블루 액센트, var(--) 주입 구조 유지) +3. ✅ VoiceRecordingPanel → GradientWave 교체 +4. 검증: tsc 0 / vitest 41/41 / 실화면 캡처 (Midnight·Nord 두 테마) +5. 잔여(후속): 라이트 테마 정밀 검수, @mui/icons-material 의존성 package.json 제거, MetalDial(미사용) 정리, v0.2.0-alpha 릴리스 ## 모델 현대화 트랙 (2026-07-21) — B 완료, A 완료(코드), C 완료(코드)