1
0
Fork 0
Everything2Everything/docs/ssot/_data/design_designs.json
Yun Chan faffb8f205 feat(ui): 형식 색상 + 손실/상태 시맨틱 토큰 (Catppuccin/Material/Radix 영감)
디자인 리서치(VS Code Material Icon Theme·Seti UI·Linguist·Radix dark·Catppuccin·Linear·Geist) 기반으로 형식 색상과 시맨틱 토큰을 확장.

- 신규 형식색 FsFmtCsv/Xlsx/Json/Svg/Video/Audio (회색→카테고리별 hue) + AllFormats/FormatPalette 매핑
- 손실 등급 토큰 FsLoss*(Lossless=초록/Container=회색/Recode=앰버/Rasterize=빨강, Radix dark)
- 상태 토큰 FsStatus*(Success/Warn/Danger/Info × dot/text/bg)
- Geist식 8% 투명 보더 FsBorderHairline
2026-06-01 14:32:59 +09:00

450 lines
No EOL
76 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

[
{
"key": "formats",
"area": "FormatShift 형식 색상·카테고리 시스템 (FsFmt* 토큰 + AllFormats / FormatPalette 매핑)",
"inspiredBy": "파일 형식 hue 컨벤션은 VS Code Material Icon Theme(실제 SVG fill: table=#8BC34A, json=#F9A825, video=#FF9800, svg=#FFB300), Seti UI(Solarized: csv/xlsx=green, audio/svg=purple, video=pink), GitHub Linguist(svg=#FF9900 교차검증)에서 차용. 단, 이 앱의 형식색은 32x28px 솔리드 배지 위 흰 텍스트(Foreground=White)로 쓰이므로 — Catppuccin/Radix step11 파스텔(텍스트 라벨용, 흰 텍스트엔 너무 밝음)이 아니라 — 기존 FsFmt 14색과 동일한 Chakra/Tailwind 500 솔리드 톤에 채도·명도를 맞춰 재조정. 다크 배경(#090A0C~#1C1E22)에서 한 칩만 튀지 않도록 Linear의 'OKLCH 균일 명도' 원칙을 적용해 6색을 동급 luminance로 정렬. 충돌 회피(file-format-colors 토픽의 '최종 매핑')에 따라 기존 14색과 겹치지 않게 hue를 배정: PDF가 이미 red라 영상=코랄, PNG/GIF가 purple이라 오디오=라이트퍼플, HEIC가 amber라 JSON=더 노란 골드.",
"proposal": "회색(FsFmtOther)으로 방치돼 있던 신규 16개 형식(csv/json/xlsx/svg/mp4·webm·mkv·mov·avi/mp3·aac·m4a·opus·ogg·flac·wav)을 6개 카테고리 색으로 점등. 카테고리 단위로 묶어 '데이터/표=그린(csv #68A063, xlsx #38A169), 데이터/구조=골드(json #E3B341), 벡터=핑크마젠타(svg #E05A9C), 영상=코랄(#FF7043, mp4·webm·mkv·mov·avi 공유), 오디오=라이트퍼플(#B794F4, mp3·aac·m4a·opus·ogg·flac·wav 공유)'로 배정. 색상환을 노랑→그린→핑크→코랄→퍼플로 넓게 띄워 인접 형식이 헷갈리지 않게 함. 6색 모두 Tailwind 400~500 luminance라 기존 14색과 한 세트로 보이고, 출력 배지(OutputFormatBadge)와 입력 큐 row 배지 양쪽에서 동일 hue로 일관되게 표시.",
"implementationSteps": [
"FormatShiftTheme.xaml의 FsFmtOther(41행) 다음에 신규 6개 SolidColorBrush 토큰 블록 추가: FsFmtCsv #68A063, FsFmtXlsx #38A169, FsFmtJson #E3B341, FsFmtSvg #E05A9C, FsFmtVideo #FF7043, FsFmtAudio #B794F4. 각 토큰에 출처(Material/Seti/Linguist) 주석 명시.",
"MainWindow.xaml.cs의 AllFormats 배열(813~843행)에서 신규 16개 항목의 4번째 인자 colorResource를 'FsFmtOther' → 카테고리 키로 교체(csv→FsFmtCsv, json→FsFmtJson, xlsx→FsFmtXlsx, svg→FsFmtSvg, 영상 5종→FsFmtVideo, 오디오 7종→FsFmtAudio). 이로써 TARGET FORMAT 사이드바 배지(UpdateOutputFormatBadge)가 카테고리 색으로 점등.",
"MainWindow.xaml.cs의 FormatPalette.For() switch(1148~1164행)에 동일 매핑 추가 — 입력 파일이 큐 row / Past Results / 미리보기 메타에서도 같은 색 배지로 보이도록(QueueItem.FromPath·HistoryRow.From가 이 함수를 사용). xls·m4v·oga 등 형제 확장자도 같은 카테고리로 포함.",
"dotnet build로 검증 — 경고 0개 / 오류 0개 확인 완료."
],
"colorTokens": [
{
"name": "FsFmtCsv",
"hex": "#68A063",
"usage": "CSV (데이터/표) — Material table=#8BC34A·Seti green 차용, xlsx보다 살짝 라이트한 라임그린으로 명도 분리"
},
{
"name": "FsFmtXlsx",
"hex": "#38A169",
"usage": "Excel/XLSX (스프레드시트) — Excel 브랜드 그린과 정합, 기존 webp(#48BB78)보다 딥하게 구분"
},
{
"name": "FsFmtJson",
"hex": "#E3B341",
"usage": "JSON (데이터/구조) — Material json=#F9A825·Seti yellow 컨벤션, 기존 HEIC(#D69E2E)보다 노랑쪽 골드로 충돌 회피"
},
{
"name": "FsFmtSvg",
"hex": "#E05A9C",
"usage": "SVG (벡터) — Seti svg=purple/css=pink 계열, 노랑 JSON과 한 화면에서 즉시 구분되는 핑크마젠타"
},
{
"name": "FsFmtVideo",
"hex": "#FF7043",
"usage": "영상 mp4/webm/mkv/mov/avi (공유) — 통념 '영상=난색', Material video=#FF9800 기반 코랄. 기존 PDF red(#E53E3E)와 구분"
},
{
"name": "FsFmtAudio",
"hex": "#B794F4",
"usage": "오디오 mp3/aac/m4a/opus/ogg/flac/wav (공유) — Seti audio=purple 통념, 기존 PNG/GIF purple보다 라이트한 바이올렛"
}
],
"xamlSnippets": [
{
"purpose": "FormatShiftTheme.xaml — FsFmtOther 다음에 추가한 신규 카테고리 형식색 토큰 (출처 주석 포함)",
"xaml": "<!-- Format pills (신규: 데이터/벡터/영상/오디오 카테고리) -->\n<!-- 출처: VS Code Material Icon Theme + Seti UI(Solarized) + GitHub Linguist 의 hue 컨벤션을\n 차용하되, 채도/명도는 기존 Chakra/Tailwind 500 솔리드 톤(흰 텍스트 위 대비)에 맞춰 재조정.\n Linear OKLCH 균일 명도 원칙(다크 배경에서 한 칩만 튀지 않게)을 적용. -->\n<!-- 데이터/표 = 그린 (Material table=#8BC34A, Seti green, Excel 브랜드 그린) -->\n<SolidColorBrush x:Key=\"FsFmtCsv\" Color=\"#68A063\"/>\n<SolidColorBrush x:Key=\"FsFmtXlsx\" Color=\"#38A169\"/>\n<!-- 데이터/구조 = 골드 (Material json=#F9A825, Seti yellow) -->\n<SolidColorBrush x:Key=\"FsFmtJson\" Color=\"#E3B341\"/>\n<!-- 벡터 = 핑크-마젠타 (Seti svg=purple/css=pink); 노랑 JSON과 즉시 구분 -->\n<SolidColorBrush x:Key=\"FsFmtSvg\" Color=\"#E05A9C\"/>\n<!-- 영상 = 코랄/오렌지레드 (Material video=#FF9800); 기존 PDF와 구분 -->\n<SolidColorBrush x:Key=\"FsFmtVideo\" Color=\"#FF7043\"/>\n<!-- 오디오 = 라이트 퍼플 (Seti audio=purple); 기존 PNG/GIF보다 라이트 -->\n<SolidColorBrush x:Key=\"FsFmtAudio\" Color=\"#B794F4\"/>"
},
{
"purpose": "MainWindow.xaml.cs — AllFormats 매핑 갱신 (FsFmtOther → 카테고리 색)",
"xaml": "new(\".csv\", \"CSV\", \"CSV\", \"FsFmtCsv\"),\nnew(\".json\", \"JSON\", \"JSON\", \"FsFmtJson\"),\nnew(\".xlsx\", \"Excel\", \"XLSX\", \"FsFmtXlsx\"),\nnew(\".svg\", \"SVG\", \"SVG\", \"FsFmtSvg\"),\nnew(\".mp4\", \"MP4\", \"MP4\", \"FsFmtVideo\"),\nnew(\".webm\", \"WebM\", \"WEBM\", \"FsFmtVideo\"),\nnew(\".mkv\", \"MKV\", \"MKV\", \"FsFmtVideo\"),\nnew(\".mov\", \"MOV\", \"MOV\", \"FsFmtVideo\"),\nnew(\".avi\", \"AVI\", \"AVI\", \"FsFmtVideo\"),\nnew(\".mp3\", \"MP3\", \"MP3\", \"FsFmtAudio\"),\nnew(\".aac\", \"AAC\", \"AAC\", \"FsFmtAudio\"),\nnew(\".m4a\", \"M4A\", \"M4A\", \"FsFmtAudio\"),\nnew(\".opus\", \"Opus\", \"OPUS\", \"FsFmtAudio\"),\nnew(\".ogg\", \"OGG\", \"OGG\", \"FsFmtAudio\"),\nnew(\".flac\", \"FLAC\", \"FLAC\", \"FsFmtAudio\"),\nnew(\".wav\", \"WAV\", \"WAV\", \"FsFmtAudio\"),"
},
{
"purpose": "MainWindow.xaml.cs — FormatPalette.For() 입력 확장자 매핑 추가 (큐 row/이력 배지용)",
"xaml": "// 신규 카테고리 (output AllFormats 매핑과 동일 hue 유지)\n\"csv\" => (\"CSV\", \"FsFmtCsv\"),\n\"json\" => (\"JSON\", \"FsFmtJson\"),\n\"xlsx\" or \"xls\" => (\"XLSX\", \"FsFmtXlsx\"),\n\"svg\" => (\"SVG\", \"FsFmtSvg\"),\n\"mp4\" or \"webm\" or \"mkv\" or \"mov\" or \"avi\" or \"m4v\"\n => (ext.ToUpperInvariant(), \"FsFmtVideo\"),\n\"mp3\" or \"aac\" or \"m4a\" or \"opus\" or \"ogg\" or \"oga\" or \"flac\" or \"wav\"\n => (ext.ToUpperInvariant(), \"FsFmtAudio\"),"
}
],
"userDecisions": [
"형식색을 Catppuccin 파스텔(2순위 권고)이 아니라 기존 14색과 동일한 Chakra/Tailwind 500 솔리드 톤으로 맞췄습니다 — 이 앱의 형식색은 흰 텍스트(Foreground=White) 솔리드 배지 배경으로 쓰이기 때문입니다. 만약 향후 형식색을 '라벨/텍스트'로도 쓰려면(예: row 우측 형식명) 그때는 Catppuccin/Radix step11 파스텔로 전체 14+6색 일괄 교체가 더 적합합니다 — 진행할까요?",
"JSON 골드(#E3B341)는 흰 텍스트 대비가 다소 낮은 경계 케이스입니다. 다만 기존 FsFmtHeic(#D69E2E)가 이미 동급 골드에 흰 텍스트를 쓰고 있어 일관성은 유지됩니다. 가독성을 더 높이려면 JSON 배지에 한해 어두운 텍스트(#1C1E22)를 쓰는 옵션도 있습니다 — 적용할까요?",
"리서치 권고에 있던 '배경/텍스트 3계층 Catppuccin/Radix 재정렬', '보더 8% 투명도화', 'OKLCH 14색 전체 재정렬', 'hover 마이크로인터랙션', '큐 row 좌측 카테고리 accent bar', 'empty state 개선'은 이번 과제 범위(신규 형식 색 부여) 밖이라 적용하지 않았습니다. 별도 작업으로 진행을 원하시면 알려주세요."
]
},
{
"key": "settings",
"area": "AI API 키 설정 창(SettingsWindow.xaml/.cs) + 외부 도구(FFmpeg/LibreOffice) 미설치 안내 + MainWindow 우상단 ⚙ 진입점",
"inspiredBy": "Cursor(Settings → API Keys: 마스킹 입력 + 명시적 Verify 버튼 + 성공/실패 인라인 배지 — \"검증 피드백 없으면 사용자가 키 적용을 불신한다\"), Zed(OS 키체인/DPAPI 암호화 저장 + 환경변수 자동 감지 후 \"환경변수에서 감지됨\" 보조 텍스트 노출), Raycast(Form.PasswordField 전용 보안 필드 + 'sk-...' placeholder 힌트, List/Detail 좌-우 섹션 구조), Microsoft Fluent/WinUI InfoBar(외부 의존성 미설치 안내를 Severity Warning/Success로 일괄 처리 — 다크에선 원색 대신 surface 위 subtle tint), Radix Colors dark scales(상태 시맨틱 12-step: solid=step9 / 텍스트=step11 / subtle 배경=step3 — Success #30A46C·#3DD68C·#132D21, Warning #FFC53D·#FFCA16·#302008, Danger #E5484D·#FF9592·#3B1219, Info #0090FF·#70B8FF·#0D2847), Linear(중립색 위주 + 사이드바를 더 어둡게 / 본문 작업영역을 밝게 하는 위계, radius 6px 통일, status dot 8px + 6px gap), Vercel Geist(보더는 rgba(255,255,255,0.08) 8% 알파로 거의 안 보이게, 그림자 대신 보더로 패널 분리), Apple HIG(2~5개 상호배타 즉시전환=Segmented Control, on/off=ToggleSwitch, 5개 초과=Select 드롭다운 기준). 기존 FsBg/FsText/FsAccent/FsFmt 토큰 체계와 FsSegmentStyle·FsPrimaryButtonStyle·FsPathInputStyle을 그대로 계승.",
"proposal": "기존 DiagnoseWindow처럼 ui:FluentWindow 모달로 신규 SettingsWindow(640x720)를 만들되, Mica 대신 MainWindow와 동일한 FsBgBase 다크 톤 + FormatShiftTheme.xaml 머지로 일관성을 유지한다. 화면은 3개 섹션 카드(FsBgSurface #1C1E22, radius 6px, 보더 rgba 8%)로 구성한다.\n\n(1) AI 백엔드 섹션 — Cursor/Zed 패턴. '활성 백엔드'는 Segmented Control(Auto / OpenAI / Anthropic, 기존 FsSegmentStyle 재사용)로 즉시 전환. OpenAI·Anthropic 각각 한 행에 [PasswordBox(마스킹, placeholder 'sk-...' / 'sk-ant-...')] + [👁 평문토글] + [Verify 버튼]. PasswordBox는 FsPathInputStyle을 PasswordBox용으로 포팅한 FsPasswordInputStyle(FsBgInput #0F1012, FsFontMono, radius 6px)을 쓴다. Verify 결과는 우측에 status dot(8px) + 라벨 배지로: 성공=Green dot #30A46C + 'Valid'(#3DD68C / 배경 #132D21), 실패=Red #E5484D + 인라인 에러(#FF9592 / 배경 #3B1219), 검증중=indeterminate ProgressBar(180x3, FsAccentBlue). 환경변수(OPENAI_API_KEY/ANTHROPIC_API_KEY)가 잡히면 입력란 위에 '환경변수에서 감지됨' 보조텍스트(#8B909A=FsTextSecondary) + Info dot #0090FF. 검증 전 빈 필드엔 에러를 표시하지 않는다(premature validation 회피).\n\n(2) 모델 섹션 — 백엔드별 모델 ComboBox(OpenAI: gpt-4o-mini/gpt-4o, Anthropic: claude-3-5-sonnet-latest/claude-3-5-haiku-latest). 선택값은 ConvertOptions.AiOptions.Model로 흐르고, '비워두면 백엔드 기본 모델 사용' 캡션을 단다.\n\n(3) 외부 도구 섹션 — Fluent InfoBar 패턴을 FsFmt 톤으로 직접 그린 StatusRow 3개(FFmpeg / LibreOffice / MS Word COM). 각 행: 좌측 status dot + 도구명 + 상태 라벨 + (미설치 시) Warning subtle tint 배경(#302008) + 단일 '다운로드' CTA(FsPrimaryButtonStyle, 형식별 색은 영상=#FF7043 FFmpeg, 문서=#38A169 LibreOffice) + '이미 설치됨? 경로 지정' 보조 링크. 감지되면 Green Success 행으로 전환('준비됨' #3DD68C). ExternalToolDetector를 internal→public 노출하거나 App.Engine.Providers 점검 결과를 재사용해 채운다.\n\n저장은 ISettingsStore(DpapiSettingsStore) 단일 인스턴스를 App에서 노출해 LlmProvider와 공유 — 키 'openai.apikey'/'anthropic.apikey', 백엔드 'ai.backend', 모델 'ai.model'. 평문 settings.json 금지(이미 DPAPI 암호화로 보장됨). MainWindow 네비바 우상단 Actions StackPanel 맨 앞에 ⚙ 아이콘 버튼(FsSecondaryButtonStyle, 32x32, Path gear)을 추가해 SettingsWindow를 Owner=this 모달로 띄운다.\n\n상태 토큰은 FsFmt 신규색(#FF7043 video, #38A169 xlsx, #E3B341 json 등)과 함께 Radix dark 시맨틱 6종(Success/Warning/Danger/Info의 solid·text·bg)을 FormatShiftTheme.xaml에 추가해 배지/dot/InfoBar 전반에서 재사용한다.",
"colorTokens": [
{
"name": "FsStatusSuccess (Radix Green9 solid · dot/CTA)",
"hex": "#30A46C",
"usage": "Verify 성공 dot, 외부도구 '준비됨' dot, Save 버튼 호버"
},
{
"name": "FsStatusSuccessText (Green11 · 배지 텍스트)",
"hex": "#3DD68C",
"usage": "'Valid' / '준비됨' 라벨 텍스트 (다크 #1C1E22 위 4.5:1+)"
},
{
"name": "FsStatusSuccessBg (Green3 · subtle fill)",
"hex": "#132D21",
"usage": "성공 배지/Success InfoBar 행 배경"
},
{
"name": "FsStatusWarn (Radix Amber9 solid)",
"hex": "#FFC53D",
"usage": "미설치 경고 dot, InfoBar 아이콘"
},
{
"name": "FsStatusWarnText (Amber11)",
"hex": "#FFCA16",
"usage": "'미설치'/'점검 필요' 라벨 텍스트"
},
{
"name": "FsStatusWarnBg (Amber3 · InfoBar tint)",
"hex": "#302008",
"usage": "FFmpeg/LibreOffice 미설치 안내 행 배경 (subtle tint)"
},
{
"name": "FsStatusDanger (Radix Red9 solid)",
"hex": "#E5484D",
"usage": "Verify 실패 dot, 변환불가 표시"
},
{
"name": "FsStatusDangerText (Red11)",
"hex": "#FF9592",
"usage": "키 검증 실패 인라인 에러 텍스트"
},
{
"name": "FsStatusDangerBg (Red3 · subtle fill)",
"hex": "#3B1219",
"usage": "실패 배지/에러 박스 배경"
},
{
"name": "FsStatusInfo (Radix Blue9 solid)",
"hex": "#0090FF",
"usage": "'환경변수에서 감지됨' dot, 정보성 표시"
},
{
"name": "FsStatusInfoText (Blue11)",
"hex": "#70B8FF",
"usage": "환경변수 감지 보조텍스트 강조 시"
},
{
"name": "FsStatusInfoBg (Blue3)",
"hex": "#0D2847",
"usage": "정보성 배지 배경"
},
{
"name": "FsFmtVideo (FFmpeg 카테고리 · Material/Seti video)",
"hex": "#FF7043",
"usage": "FFmpeg 다운로드 CTA / 영상 형식칩 (pdf #E53E3E와 구분되는 코랄)"
},
{
"name": "FsFmtXlsx (LibreOffice/문서 · 엑셀 그린)",
"hex": "#38A169",
"usage": "LibreOffice 다운로드 CTA / 문서 형식 강조"
},
{
"name": "FsFmtJson (데이터 · Material json 골드)",
"hex": "#E3B341",
"usage": "신규 데이터 형식칩 (회색 방치 형식 정상화, 동시 적용 권장)"
},
{
"name": "FsBorderAlpha8 (Geist 8% 보더)",
"hex": "#14FFFFFF",
"usage": "설정 카드/섹션 구분 1px 보더 (rgba(255,255,255,0.08) ARGB 표기)"
},
{
"name": "FsTextSecondary (기존 · 보조텍스트, 변경 없음)",
"hex": "#8B909A",
"usage": "'환경변수에서 감지됨'/'비워두면 기본 모델' 캡션 (Zed 패턴)"
}
],
"xamlSnippets": [
{
"purpose": "FormatShiftTheme.xaml에 추가할 Radix dark 상태 시맨틱 토큰 + FFmpeg/LibreOffice 형식색 (기존 FsFmt* 패턴 계승, 28~41행 뒤에 삽입)",
"xaml": "<!-- Status semantics (Radix dark step9/11/3) -->\n<SolidColorBrush x:Key=\"FsStatusSuccess\" Color=\"#30A46C\"/>\n<SolidColorBrush x:Key=\"FsStatusSuccessText\" Color=\"#3DD68C\"/>\n<SolidColorBrush x:Key=\"FsStatusSuccessBg\" Color=\"#132D21\"/>\n<SolidColorBrush x:Key=\"FsStatusWarn\" Color=\"#FFC53D\"/>\n<SolidColorBrush x:Key=\"FsStatusWarnText\" Color=\"#FFCA16\"/>\n<SolidColorBrush x:Key=\"FsStatusWarnBg\" Color=\"#302008\"/>\n<SolidColorBrush x:Key=\"FsStatusDanger\" Color=\"#E5484D\"/>\n<SolidColorBrush x:Key=\"FsStatusDangerText\" Color=\"#FF9592\"/>\n<SolidColorBrush x:Key=\"FsStatusDangerBg\" Color=\"#3B1219\"/>\n<SolidColorBrush x:Key=\"FsStatusInfo\" Color=\"#0090FF\"/>\n<SolidColorBrush x:Key=\"FsStatusInfoText\" Color=\"#70B8FF\"/>\n<SolidColorBrush x:Key=\"FsStatusInfoBg\" Color=\"#0D2847\"/>\n<!-- New format-category colors (회색 방치 형식 정상화) -->\n<SolidColorBrush x:Key=\"FsFmtVideo\" Color=\"#FF7043\"/>\n<SolidColorBrush x:Key=\"FsFmtAudio\" Color=\"#B794F4\"/>\n<SolidColorBrush x:Key=\"FsFmtXlsx\" Color=\"#38A169\"/>\n<SolidColorBrush x:Key=\"FsFmtJson\" Color=\"#E3B341\"/>\n<SolidColorBrush x:Key=\"FsFmtCsv\" Color=\"#68A063\"/>\n<SolidColorBrush x:Key=\"FsFmtSvg\" Color=\"#E05A9C\"/>"
},
{
"purpose": "PasswordBox 전용 입력 스타일 (FsPathInputStyle을 PasswordBox용으로 포팅 — PART_ContentHost 동일 사용 가능)",
"xaml": "<Style x:Key=\"FsPasswordInputStyle\" TargetType=\"PasswordBox\">\n <Setter Property=\"Background\" Value=\"{StaticResource FsBgInput}\"/>\n <Setter Property=\"Foreground\" Value=\"{StaticResource FsTextPrimary}\"/>\n <Setter Property=\"BorderBrush\" Value=\"{StaticResource FsBorderSubtle}\"/>\n <Setter Property=\"BorderThickness\" Value=\"1\"/>\n <Setter Property=\"Padding\" Value=\"10,8\"/>\n <Setter Property=\"FontFamily\" Value=\"{StaticResource FsFontMono}\"/>\n <Setter Property=\"FontSize\" Value=\"12\"/>\n <Setter Property=\"PasswordChar\" Value=\"●\"/>\n <Setter Property=\"CaretBrush\" Value=\"{StaticResource FsAccentBlue}\"/>\n <Setter Property=\"Template\">\n <Setter.Value>\n <ControlTemplate TargetType=\"PasswordBox\">\n <Border Background=\"{TemplateBinding Background}\"\n BorderBrush=\"{TemplateBinding BorderBrush}\"\n BorderThickness=\"{TemplateBinding BorderThickness}\"\n CornerRadius=\"6\" Padding=\"{TemplateBinding Padding}\">\n <ScrollViewer x:Name=\"PART_ContentHost\"/>\n </Border>\n <ControlTemplate.Triggers>\n <Trigger Property=\"IsKeyboardFocusWithin\" Value=\"True\">\n <Setter Property=\"BorderBrush\" Value=\"{StaticResource FsAccentBlue}\"/>\n </Trigger>\n </ControlTemplate.Triggers>\n </ControlTemplate>\n </Setter.Value>\n </Style.Setter>\n</Style>"
},
{
"purpose": "SettingsWindow.xaml — 창 셸 + API 키 한 행(PasswordBox + 👁 토글 + Verify + status dot 배지). Cursor/Zed/Raycast 패턴 집약",
"xaml": "<ui:FluentWindow x:Class=\"Everything2Everything.App.Views.SettingsWindow\"\n xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\n xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n xmlns:ui=\"http://schemas.lepo.co/wpfui/2022/xaml\"\n Title=\"설정\" Width=\"640\" Height=\"720\"\n ExtendsContentIntoTitleBar=\"True\" WindowBackdropType=\"None\"\n WindowCornerPreference=\"Round\" WindowStartupLocation=\"CenterOwner\">\n <Window.Resources>\n <ResourceDictionary>\n <ResourceDictionary.MergedDictionaries>\n <ResourceDictionary Source=\"FormatShiftTheme.xaml\"/>\n </ResourceDictionary.MergedDictionaries>\n </ResourceDictionary>\n </Window.Resources>\n <Grid Background=\"{StaticResource FsBgBase}\">\n <Grid.RowDefinitions>\n <RowDefinition Height=\"32\"/><RowDefinition Height=\"*\"/><RowDefinition Height=\"Auto\"/>\n </Grid.RowDefinitions>\n <ui:TitleBar Grid.Row=\"0\" Title=\"\"/>\n <ScrollViewer Grid.Row=\"1\" Padding=\"24\" VerticalScrollBarVisibility=\"Auto\">\n <StackPanel>\n <!-- AI 백엔드 카드 -->\n <Border Background=\"{StaticResource FsBgSurface}\" CornerRadius=\"6\"\n BorderBrush=\"#14FFFFFF\" BorderThickness=\"1\" Padding=\"20\">\n <StackPanel>\n <TextBlock Text=\"AI BACKEND\" Style=\"{StaticResource FsLabelStyle}\"/>\n <Border Margin=\"0,8,0,16\" Background=\"{StaticResource FsBgInput}\"\n BorderBrush=\"{StaticResource FsBorderSubtle}\" BorderThickness=\"1\"\n CornerRadius=\"6\" Padding=\"2\">\n <UniformGrid Rows=\"1\" Columns=\"3\">\n <ToggleButton x:Name=\"BackendAutoBtn\" Content=\"Auto\" IsChecked=\"True\"\n Style=\"{StaticResource FsSegmentStyle}\" Tag=\"auto\"\n Click=\"OnBackendSegmentClick\"/>\n <ToggleButton x:Name=\"BackendOpenAiBtn\" Content=\"OpenAI\"\n Style=\"{StaticResource FsSegmentStyle}\" Tag=\"openai\"\n Click=\"OnBackendSegmentClick\"/>\n <ToggleButton x:Name=\"BackendAnthropicBtn\" Content=\"Anthropic\"\n Style=\"{StaticResource FsSegmentStyle}\" Tag=\"anthropic\"\n Click=\"OnBackendSegmentClick\"/>\n </UniformGrid>\n </Border>\n <!-- OpenAI 키 행 -->\n <TextBlock Text=\"OPENAI API KEY\" Style=\"{StaticResource FsLabelStyle}\" Margin=\"0,0,0,4\"/>\n <TextBlock x:Name=\"OpenAiEnvHint\" Style=\"{StaticResource FsCaptionStyle}\"\n Foreground=\"{StaticResource FsTextSecondary}\" Margin=\"0,0,0,6\"\n Visibility=\"Collapsed\" Text=\"● 환경변수 OPENAI_API_KEY에서 감지됨\"/>\n <Grid>\n <Grid.ColumnDefinitions>\n <ColumnDefinition Width=\"*\"/><ColumnDefinition Width=\"Auto\"/>\n <ColumnDefinition Width=\"Auto\"/>\n </Grid.ColumnDefinitions>\n <PasswordBox x:Name=\"OpenAiKeyBox\" Style=\"{StaticResource FsPasswordInputStyle}\"/>\n <ToggleButton x:Name=\"OpenAiРeveal\" Grid.Column=\"1\" Width=\"36\" Margin=\"8,0,0,0\"\n Content=\"👁\" Style=\"{StaticResource FsSegmentStyle}\"\n Click=\"OnToggleRevealOpenAi\"/>\n <Button Grid.Column=\"2\" Margin=\"8,0,0,0\" Content=\"Verify\"\n Style=\"{StaticResource FsSecondaryButtonStyle}\"\n Click=\"OnVerifyOpenAiClick\"/>\n </Grid>\n <!-- Verify 결과 배지 (성공/실패/검증중) -->\n <StackPanel x:Name=\"OpenAiStatusRow\" Orientation=\"Horizontal\" Margin=\"0,8,0,0\"\n Visibility=\"Collapsed\">\n <Ellipse x:Name=\"OpenAiDot\" Width=\"8\" Height=\"8\" VerticalAlignment=\"Center\"\n Fill=\"{StaticResource FsStatusSuccess}\"/>\n <TextBlock x:Name=\"OpenAiStatusText\" Margin=\"6,0,0,0\" FontSize=\"12\"\n Foreground=\"{StaticResource FsStatusSuccessText}\" Text=\"Valid\"/>\n </StackPanel>\n </StackPanel>\n </Border>\n <!-- (Anthropic 키 카드 · 모델 카드 · 외부도구 카드 동일 패턴 반복) -->\n </StackPanel>\n </ScrollViewer>\n <!-- 푸터: 저장/닫기 -->\n <Border Grid.Row=\"2\" Padding=\"24,16\" BorderBrush=\"#14FFFFFF\" BorderThickness=\"0,1,0,0\">\n <StackPanel Orientation=\"Horizontal\" HorizontalAlignment=\"Right\">\n <Button Content=\"닫기\" Margin=\"0,0,8,0\" Style=\"{StaticResource FsSecondaryButtonStyle}\"\n Click=\"OnCloseClick\"/>\n <Button Content=\"저장\" Width=\"96\" Style=\"{StaticResource FsPrimaryButtonStyle}\"\n Click=\"OnSaveClick\"/>\n </StackPanel>\n </Border>\n </Grid>\n</ui:FluentWindow>"
},
{
"purpose": "외부 도구 미설치 안내 행 (Fluent InfoBar를 FsFmt 톤으로 — Warning tint 배경 + 단일 다운로드 CTA + 경로 지정 보조 링크)",
"xaml": "<Border x:Name=\"FfmpegRow\" CornerRadius=\"6\" Padding=\"14,12\" Margin=\"0,8,0,0\"\n Background=\"{StaticResource FsStatusWarnBg}\">\n <Grid>\n <Grid.ColumnDefinitions>\n <ColumnDefinition Width=\"Auto\"/><ColumnDefinition Width=\"*\"/>\n <ColumnDefinition Width=\"Auto\"/>\n </Grid.ColumnDefinitions>\n <Ellipse Width=\"8\" Height=\"8\" VerticalAlignment=\"Center\"\n Fill=\"{StaticResource FsStatusWarn}\"/>\n <StackPanel Grid.Column=\"1\" Margin=\"10,0,0,0\" VerticalAlignment=\"Center\">\n <TextBlock Text=\"FFmpeg — 영상/오디오 변환에 필요\" Style=\"{StaticResource FsBodyStyle}\"\n FontSize=\"12\" FontWeight=\"Medium\"/>\n <TextBlock x:Name=\"FfmpegReason\" Style=\"{StaticResource FsCaptionStyle}\"\n Foreground=\"{StaticResource FsStatusWarnText}\" Margin=\"0,2,0,0\"\n Text=\"미설치 — ffmpeg.exe/ffprobe.exe를 찾을 수 없습니다\"/>\n <TextBlock Margin=\"0,4,0,0\">\n <Hyperlink Click=\"OnSetFfmpegPathClick\" Foreground=\"{StaticResource FsTextSecondary}\">\n <Run Text=\"이미 설치됨? 경로 지정\"/></Hyperlink>\n </TextBlock>\n </StackPanel>\n <Button Grid.Column=\"2\" Content=\"다운로드\" VerticalAlignment=\"Center\"\n Background=\"{StaticResource FsFmtVideo}\" Foreground=\"White\"\n Style=\"{StaticResource FsPrimaryButtonStyle}\" Padding=\"12,6\"\n Click=\"OnDownloadFfmpegClick\"/>\n </Grid>\n</Border>"
},
{
"purpose": "MainWindow.xaml 네비바 우상단 ⚙ 진입점 (336행 Actions StackPanel 맨 앞에 삽입)",
"xaml": "<Button x:Name=\"SettingsButton\" Width=\"32\" Margin=\"0,0,8,0\"\n Style=\"{StaticResource FsSecondaryButtonStyle}\" Padding=\"6\"\n ToolTip=\"설정 (AI 키 · 외부 도구)\"\n Click=\"OnSettingsClick\">\n <Path Width=\"16\" Height=\"16\" Stretch=\"Uniform\"\n Stroke=\"{StaticResource FsTextSecondary}\" StrokeThickness=\"2\"\n Data=\"M12,9 A3,3 0 1,1 12,15 A3,3 0 1,1 12,9 Z M19.4,13 A7.5,7.5 0 0,0 19.4,11 L21.5,9.4 L19.5,6 L17,7 A7.5,7.5 0 0,0 15,5.8 L14.6,3 L9.4,3 L9,5.8 A7.5,7.5 0 0,0 7,7 L4.5,6 L2.5,9.4 L4.6,11 A7.5,7.5 0 0,0 4.6,13 L2.5,14.6 L4.5,18 L7,17 A7.5,7.5 0 0,0 9,18.2 L9.4,21 L14.6,21 L15,18.2 A7.5,7.5 0 0,0 17,17 L19.5,18 L21.5,14.6 Z\"/>\n</Button>"
}
],
"implementationSteps": [
"FormatShiftTheme.xaml: Radix dark 상태 시맨틱 12토큰(FsStatusSuccess/Warn/Danger/Info × solid/text/bg) + 신규 형식색(FsFmtVideo #FF7043, FsFmtAudio #B794F4, FsFmtXlsx #38A169, FsFmtJson #E3B341, FsFmtCsv #68A063, FsFmtSvg #E05A9C) 추가. FsPasswordInputStyle(PasswordBox용) 스타일 추가 — 기존 FsPathInputStyle 구조 복제 + PasswordChar ● + 포커스 시 FsAccentBlue 보더",
"App.xaml.cs: 현재 Bootstrap이 내부에서 DpapiSettingsStore를 생성하므로, Everything2EverythingBootstrap.CreateDefault()가 ISettingsStore를 외부에서 받거나 반환하도록 시그니처를 확장하고 App에 'public ISettingsStore Settings { get; }'를 노출해 LlmProvider와 SettingsWindow가 동일 인스턴스를 공유 (키 'openai.apikey'/'anthropic.apikey'/'ai.backend'/'ai.model')",
"ExternalToolDetector를 internal→public으로 승격(또는 얇은 public ToolStatusProbe 래퍼 신설)해 TryFindFfmpeg/TryFindLibreOfficeSoffice/IsWordComAvailable를 App 계층에서 호출 가능하게 함. 결과를 Settings 외부도구 섹션의 status dot/배지에 매핑(준비됨=Success, 미설치=Warning)",
"SettingsWindow.xaml 신규: 32px TitleBar + ScrollViewer 3카드(AI 백엔드/모델/외부도구) + 푸터(저장·닫기). 카드는 FsBgSurface #1C1E22 + 보더 #14FFFFFF(8% 알파) + radius 6px. 백엔드는 FsSegmentStyle 3-segment, 키는 PasswordBox+👁토글+Verify 행 + status 배지, 외부도구는 InfoBar식 StatusRow",
"SettingsWindow.xaml.cs 신규: 생성자에서 ((App)Application.Current).Settings로 기존 키 로드 → PasswordBox에 마스킹 표시(또는 '저장된 키 있음' placeholder), 환경변수 감지 시 EnvHint 노출. OnBackendSegmentClick(3-way 토글, ConflictSegment 패턴 차용), OnToggleReveal*(PasswordChar 토글 또는 평문 TextBox 스왑), OnVerify*Click(IChatClient.CompleteAsync로 1토큰 핑 → 성공 Green/실패 Red 배지, 검증중 indeterminate), OnSaveClick(ISettingsStore.Set), OnDownload*/OnSetPath* 핸들러",
"Verify 구현: OpenAiChatClient/AnthropicChatClient를 임시 인스턴스화해 CompleteAsync(system='ping', user='ping', maxTokens=1) 호출 → 401/403=키 무효(Danger), 200=Valid(Success), 네트워크 오류=Warning. premature validation 회피: 빈 필드는 Verify 버튼 비활성",
"외부도구 다운로드 CTA: FFmpeg→https://www.gyan.dev/ffmpeg/builds/ 또는 https://ffmpeg.org/download.html, LibreOffice→https://www.libreoffice.org/download/ 를 Process.Start로 브라우저 오픈. '경로 지정' 링크는 OpenFolderDialog로 ffmpeg 폴더를 받아 %LOCALAPPDATA%\\Everything2Everything\\ffmpeg에 매핑하거나 설정 키로 저장",
"MainWindow.xaml: 네비바 Actions StackPanel(336행) 맨 앞에 ⚙ Button(FsSecondaryButtonStyle, 32x32, gear Path) 추가. MainWindow.xaml.cs: OnSettingsClick에서 new SettingsWindow(((App)Application.Current).Settings, ((App)Application.Current).Engine){ Owner=this }.ShowDialog(); 닫힌 뒤 _ = RefreshCapabilityStatusAsync()로 키 반영"
],
"userDecisions": [
"Verify 방식: 실제 API 1토큰 핑(소액 과금 발생 가능, 가장 확실)으로 갈지 vs 키 형식 정규식 검사(sk-/sk-ant- prefix, 무과금이지만 무효 키 못 잡음)만 할지 — Cursor는 실제 검증 권장",
"모델 선택 범위: 고정 드롭다운(gpt-4o-mini/gpt-4o, claude-3-5-sonnet/haiku)로 충분한지, 아니면 자유 입력 TextBox로 임의 모델명 허용할지",
"외부도구 '경로 지정'을 SettingsWindow에서 영구 저장(ISettingsStore 'ffmpeg.dir' 키 신설)할지, 아니면 기존 %LOCALAPPDATA%\\Everything2Everything\\ffmpeg 폴더에 복사 안내만 할지 — 후자는 코드 변경 최소",
"신규 형식색(FsFmtVideo/Audio/Xlsx/Json/Csv/Svg)을 이번에 함께 적용해 MainWindow.xaml.cs의 AllFormats 'FsFmtOther' 회색 매핑까지 교체할지, 아니면 Settings 창에 필요한 상태색만 먼저 넣을지 (리서치 권고는 일괄 교체)",
"SettingsWindow를 모달(ShowDialog, DiagnoseWindow와 동일)로 둘지 vs 비모달로 둘지 — 모달이면 닫힐 때 RefreshCapabilityStatus 동기화가 단순"
]
},
{
"key": "badges",
"area": "손실 등급(Lossless / Container / Recode / Rasterize) 배지 + 상태 dot 시스템 — 출력 드롭다운 / 큐 row / capability 경고",
"inspiredBy": "Radix Colors (dark scales) — Badge의 \"같은 step 번호 = 같은 UI 역할\" 규칙(solid=step9 / text=step11 / subtle bg=step3)을 4개 손실 등급의 dot·텍스트·배경 토큰화에 그대로 적용. 실제 hex: Green9 #30A46C·Green11 #3DD68C·Green3 #132D21, Amber9 #FFC53D·Amber11 #FFCA16·Amber3 #302008, Red9 #E5484D·Red11 #FF9592·Red3 #3B1219, Blue9 #0090FF·Blue11 #70B8FF·Blue3 #0D2847 (https://www.radix-ui.com/themes/docs/components/badge). shadcn/Radix Badge — pill 형태(height 20~22px, radius 6px, h-padding 8px). Linear (status dot/pill, https://linear.app/now/how-we-redesigned-the-linear-ui) — 8px 채워진 원 + 6px gap + 라벨 패턴, 다크에서 LCH로 명도 통일해 어떤 등급이든 같은 밝기로 보이게. Microsoft Fluent / WinUI InfoBar (https://learn.microsoft.com/windows/apps/design/controls/infobar) — capability 경고를 Severity(Warning) subtle tint(step3 배경 + step11 텍스트) + 단일 CTA로. Things 3 (https://culturedcode.com/things/features/) — 큐 row hover 시에만 보조 정보 페이드인. 기존 FsFmt/FsAccent 토큰 체계와 8px spacing·6px radius는 계승.",
"proposal": "손실 등급은 이미 Core의 LossClass enum(Lossless/Container/Recode/Rasterize, ProviderRegistry.cs)에 SSOT로 존재하고 FindBestPath가 경로의 엣지별 Loss를 반환한다. 하지만 UI에는 전혀 노출되지 않아, 사용자가 \"JPG로 바꾸면 화질이 깎이는지\", \"PDF→PNG는 텍스트 편집성이 사라지는지\"를 알 수 없다. 이 4등급을 Radix Badge의 step9/11/3 3토큰 규칙으로 시각화하되, 색상은 \"심각도 그라데이션\"으로 의미 고정한다: Lossless=Green(안전), Container=Blue(중립·구조변경), Recode=Amber(주의·화질저하), Rasterize=Red(경고·편집성 상실, 단방향 절벽). 형식색(FsFmt*)은 \"무엇으로/무엇에서\"를 나타내고, 손실 등급색은 \"얼마나 보존되는가\"를 나타내는 직교(orthogonal)한 축이므로 절대 섞지 않는다. 표현 강도는 3단계로 차등: (1) 출력 드롭다운 = 선택지마다 8px dot + 짧은 라벨(Lossless/Recode 등)로 선택 전 예고, (2) 큐 row = 등급에 따라 dot만 또는 Recode/Rasterize일 때만 작은 pill 배지(과하지 않게 — Lossless/Container는 dot만), (3) capability 경고 = InfoBar Warning tint. 핵심 절제 원칙: Lossless·Container는 조용한 dot, Recode·Rasterize만 텍스트 라벨까지 노출해 \"주의가 필요할 때만 눈에 띄게\".",
"colorTokens": [
{
"name": "FsLossLosslessDot",
"hex": "#30A46C",
"usage": "Lossless 등급 dot/solid (Radix Green9). 무손실 — 픽셀·내용 완전 보존"
},
{
"name": "FsLossLosslessText",
"hex": "#3DD68C",
"usage": "Lossless 배지 텍스트 (Radix Green11). 다크 배경 4.5:1+ 대비"
},
{
"name": "FsLossLosslessBg",
"hex": "#132D21",
"usage": "Lossless pill subtle 배경 (Radix Green3)"
},
{
"name": "FsLossContainerDot",
"hex": "#0090FF",
"usage": "Container 등급 dot (Radix Blue9). 컨테이너/구조 변경, 내용 보존"
},
{
"name": "FsLossContainerText",
"hex": "#70B8FF",
"usage": "Container 배지 텍스트 (Radix Blue11)"
},
{
"name": "FsLossContainerBg",
"hex": "#0D2847",
"usage": "Container pill subtle 배경 (Radix Blue3)"
},
{
"name": "FsLossRecodeDot",
"hex": "#FFC53D",
"usage": "Recode 등급 dot (Radix Amber9). 재인코딩/손실 압축 — 주의"
},
{
"name": "FsLossRecodeText",
"hex": "#FFCA16",
"usage": "Recode 배지 텍스트 (Radix Amber11)"
},
{
"name": "FsLossRecodeBg",
"hex": "#302008",
"usage": "Recode pill subtle 배경 (Radix Amber3) — InfoBar Warning tint와 공유"
},
{
"name": "FsLossRasterizeDot",
"hex": "#E5484D",
"usage": "Rasterize 등급 dot (Radix Red9). 래스터화 — 편집성 상실, 단방향 절벽"
},
{
"name": "FsLossRasterizeText",
"hex": "#FF9592",
"usage": "Rasterize 배지 텍스트 (Radix Red11)"
},
{
"name": "FsLossRasterizeBg",
"hex": "#3B1219",
"usage": "Rasterize pill subtle 배경 (Radix Red3)"
}
],
"implementationSteps": [
"FormatShiftTheme.xaml: 기존 FsAccent* 블록(line 25) 아래에 위 12개 FsLoss* SolidColorBrush 토큰을 'Loss grade tokens (Radix dark Badge)' 주석과 함께 추가. 기존 FsFmt* 형식색은 손대지 않는다(직교 축).",
"FormatShiftTheme.xaml: 손실 dot 재사용 스타일 FsLossDotStyle(Ellipse, Width/Height 8) + pill 컨테이너 스타일 FsLossPillStyle(Border, CornerRadius 6, Padding 8,2, Height 22)를 추가. dot은 8px 원 + 6px gap 라벨(Linear 패턴).",
"MainWindow.xaml.cs: LossClass→(DotBrush, TextBrush, BgBrush, ShortLabel, FullLabel) 매핑 헬퍼 정적 클래스 LossPalette 추가(FormatPalette와 동형). ShortLabel='Lossless'/'Container'/'Recode'/'Rasterize', FullLabel은 한국어 툴팁('무손실 — 픽셀 완전 보존' 등 enum XML doc 문구 재사용).",
"MainWindow.xaml.cs: 출력 형식 선택 시 engine.Providers.Graph.FindBestPath(input, output)의 worst-case Loss를 계산하는 메서드 LossGradeFor(IEnumerable<string> sources, string outputExt) 추가 — 큐 전체 입력에 대해 각 경로의 최악 등급 중 가장 나쁜 것을 등급으로 채택(Max severity).",
"출력 드롭다운: OutputFormatCombo ItemTemplate을 dot+라벨+등급dot 3요소로 교체(현재는 'JPEG (.jpg)' 텍스트만). 각 ComboBoxItem.Tag 옆에 그 형식의 손실 등급 dot을 우측 정렬로 배치해 선택 전 예고. OutputFormatBadge 아래에 현재 선택 등급 pill 1줄 추가(LossGradeBadge x:Name).",
"큐 row: QueueItem에 LossClass 속성 + LossDotBrush/LossPillVisibility/LossLabel/LossBrush 파생 속성 추가. ItemTemplate Grid.Column 3(StateText) 영역 또는 형식 배지 우상단에 dot 배치. 절제: Lossless/Container는 dot만, Recode/Rasterize일 때만 pill 라벨을 ProgressVisibility처럼 조건부 노출(LossPillVisibility).",
"capability 경고: 현재 CapabilityStatusText(평문 TextBlock, line 233)를 Radix Amber tint InfoBar 패턴 Border로 교체 — Background FsLossRecodeBg(#302008), 좌측 경고 아이콘 + FsLossRecodeText(#FFCA16) 텍스트 + 단일 'Diagnose' CTA. RefreshCapabilityStatusAsync의 '⚠ N개 형식이...' 문자열을 이 Border 내부 TextBlock에 바인딩.",
"Rasterize 경고 강화(선택): 선택한 출력이 Rasterize 등급이면 출력 드롭다운 hint(OutputFormatHint)에 '벡터/텍스트 편집성이 사라집니다 (단방향)' 한 줄을 FsLossRasterizeText 색으로 표시해 비가역성을 사전 고지."
],
"userDecisions": [
"Container 등급 색을 Blue(#0090FF, Radix Blue9)로 둘지 vs Teal/중립 회색으로 둘지 — Blue는 앱 액센트 FsAccentBlue(#3B72FF)와 hue가 가까워 '클릭 가능'으로 오인될 여지가 있음. 대안: Container를 무채색 dot(FsTextSecondary #8B909A)으로 '조용히' 처리(구조 변경은 손실이 아니므로 색을 아예 안 주는 것도 정합적). 추천: Container=중립 회색 dot, Blue는 순수 정보(info)용으로 아껴두기.",
"큐 row에서 Recode/Rasterize pill을 '항상 노출' vs 'hover 시에만 페이드인'(Things 3 패턴) — 항상 노출은 정보 우선, hover는 미니멀 우선. 큐는 변환 전 검토 화면이므로 손실은 항상 보이는 게 안전하다고 보지만, 사용자가 미니멀을 선호하면 hover로 전환.",
"출력 드롭다운 각 항목에 등급 dot을 '모든 형식'에 표시할지 vs Recode/Rasterize에만 표시할지 — 모든 형식에 dot을 주면 일관되지만 Lossless 초록 dot이 다수라 시각 노이즈가 될 수 있음. 추천: 전부 표시하되 Lossless는 가장 낮은 채도라 자연히 묻힘(Radix가 의도한 명도 통일 효과).",
"손실 등급 라벨을 영어(Lossless/Container/Recode/Rasterize, enum 이름 그대로) vs 한국어(무손실/재포장/재인코딩/래스터화)로 표기할지 — 코드/툴팁은 영어 enum 유지, UI 가시 라벨만 한국어로 갈지 결정 필요. 앱이 한국어 hint를 쓰므로 가시 라벨도 한국어 권장, 영어는 툴팁 보조."
],
"xamlSnippets": [
{
"purpose": "FormatShiftTheme.xaml — 손실 등급 토큰 (Radix dark Badge step9/11/3). 기존 FsAccent* 블록(line 25) 바로 아래에 삽입",
"xaml": "<!-- Loss grade tokens (Radix dark Badge: dot=step9 / text=step11 / bg=step3) -->\n<!-- Lossless = Green9/11/3 -->\n<SolidColorBrush x:Key=\"FsLossLosslessDot\" Color=\"#30A46C\"/>\n<SolidColorBrush x:Key=\"FsLossLosslessText\" Color=\"#3DD68C\"/>\n<SolidColorBrush x:Key=\"FsLossLosslessBg\" Color=\"#132D21\"/>\n<!-- Container = 중립(회색 dot 권장) 또는 Blue9/11/3 -->\n<SolidColorBrush x:Key=\"FsLossContainerDot\" Color=\"#8B909A\"/>\n<SolidColorBrush x:Key=\"FsLossContainerText\" Color=\"#A6ADC8\"/>\n<SolidColorBrush x:Key=\"FsLossContainerBg\" Color=\"#1C1E22\"/>\n<!-- Recode = Amber9/11/3 (Warning) -->\n<SolidColorBrush x:Key=\"FsLossRecodeDot\" Color=\"#FFC53D\"/>\n<SolidColorBrush x:Key=\"FsLossRecodeText\" Color=\"#FFCA16\"/>\n<SolidColorBrush x:Key=\"FsLossRecodeBg\" Color=\"#302008\"/>\n<!-- Rasterize = Red9/11/3 (Danger, 단방향) -->\n<SolidColorBrush x:Key=\"FsLossRasterizeDot\" Color=\"#E5484D\"/>\n<SolidColorBrush x:Key=\"FsLossRasterizeText\" Color=\"#FF9592\"/>\n<SolidColorBrush x:Key=\"FsLossRasterizeBg\" Color=\"#3B1219\"/>"
},
{
"purpose": "FormatShiftTheme.xaml — 재사용 dot + pill 스타일 (Linear 8px dot / shadcn pill 22px h, 6px radius)",
"xaml": "<!-- 8px loss dot (Linear status dot 패턴) -->\n<Style x:Key=\"FsLossDotStyle\" TargetType=\"Ellipse\">\n <Setter Property=\"Width\" Value=\"8\"/>\n <Setter Property=\"Height\" Value=\"8\"/>\n <Setter Property=\"VerticalAlignment\" Value=\"Center\"/>\n</Style>\n\n<!-- Loss pill (Radix/shadcn Badge: h22, radius6, hpad8) -->\n<Style x:Key=\"FsLossPillStyle\" TargetType=\"Border\">\n <Setter Property=\"CornerRadius\" Value=\"6\"/>\n <Setter Property=\"Height\" Value=\"22\"/>\n <Setter Property=\"Padding\" Value=\"8,0\"/>\n <Setter Property=\"VerticalAlignment\" Value=\"Center\"/>\n</Style>"
},
{
"purpose": "MainWindow.xaml — 출력 드롭다운 아래 '현재 선택 손실 등급' pill (OutputFormatBadge 영역 line 89~101 아래, OutputFormatHint 위에 삽입). dot 8px + 6px gap + 한국어 라벨",
"xaml": "<!-- Selected output loss grade (예: Recode일 때) -->\n<Border x:Name=\"LossGradeBadge\" Margin=\"0,8,0,0\"\n HorizontalAlignment=\"Left\"\n Style=\"{StaticResource FsLossPillStyle}\"\n Background=\"{StaticResource FsLossRecodeBg}\">\n <StackPanel Orientation=\"Horizontal\" VerticalAlignment=\"Center\">\n <Ellipse x:Name=\"LossGradeDot\" Style=\"{StaticResource FsLossDotStyle}\"\n Fill=\"{StaticResource FsLossRecodeDot}\"/>\n <TextBlock x:Name=\"LossGradeLabel\" Margin=\"6,0,0,0\"\n FontFamily=\"{StaticResource FsFontSans}\" FontSize=\"11\"\n FontWeight=\"SemiBold\"\n Foreground=\"{StaticResource FsLossRecodeText}\"\n Text=\"재인코딩 (화질 손실)\"/>\n </StackPanel>\n</Border>"
},
{
"purpose": "MainWindow.xaml — 큐 row ItemTemplate: 형식 배지(line 401~409) 우상단에 손실 dot 오버레이. Lossless/Container는 dot만, Recode/Rasterize는 dot이 눈에 띄는 색",
"xaml": "<!-- 형식 배지 + 우상단 손실 등급 dot 오버레이 -->\n<Grid Width=\"32\" Height=\"32\" VerticalAlignment=\"Center\">\n <Border CornerRadius=\"4\" Background=\"{Binding FormatBrush}\">\n <TextBlock Text=\"{Binding FormatLabel}\" Foreground=\"White\"\n FontSize=\"10\" FontWeight=\"SemiBold\"\n HorizontalAlignment=\"Center\" VerticalAlignment=\"Center\"/>\n </Border>\n <Ellipse Width=\"10\" Height=\"10\"\n HorizontalAlignment=\"Right\" VerticalAlignment=\"Top\"\n Margin=\"0,-3,-3,0\"\n Stroke=\"{StaticResource FsBgPanel}\" StrokeThickness=\"2\"\n Fill=\"{Binding LossDotBrush}\"\n ToolTip=\"{Binding LossTooltip}\"/>\n</Grid>"
},
{
"purpose": "MainWindow.xaml — 큐 row의 StateText 컬럼(Grid.Column 3, line 428~432) 위/옆에 조건부 Recode·Rasterize pill (절제: Recode/Rasterize일 때만 Visible)",
"xaml": "<StackPanel Grid.Column=\"3\" Orientation=\"Horizontal\" VerticalAlignment=\"Center\">\n <Border Style=\"{StaticResource FsLossPillStyle}\" Height=\"20\"\n Background=\"{Binding LossPillBg}\"\n Visibility=\"{Binding LossPillVisibility}\"\n Margin=\"0,0,8,0\"\n ToolTip=\"{Binding LossTooltip}\">\n <StackPanel Orientation=\"Horizontal\" VerticalAlignment=\"Center\">\n <Ellipse Style=\"{StaticResource FsLossDotStyle}\" Fill=\"{Binding LossDotBrush}\"/>\n <TextBlock Text=\"{Binding LossLabel}\" Margin=\"6,0,0,0\"\n FontFamily=\"{StaticResource FsFontSans}\" FontSize=\"10\"\n FontWeight=\"SemiBold\" Foreground=\"{Binding LossTextBrush}\"/>\n </StackPanel>\n </Border>\n <TextBlock Text=\"{Binding StateText}\"\n FontFamily=\"{StaticResource FsFontMono}\" FontSize=\"12\"\n Foreground=\"{Binding StateBrush}\" VerticalAlignment=\"Center\"/>\n</StackPanel>"
},
{
"purpose": "MainWindow.xaml — capability 경고를 평문 TextBlock(line 233)에서 Radix Amber InfoBar(Fluent Severity=Warning)로 교체. subtle tint 배경 + 단일 Diagnose CTA",
"xaml": "<Border x:Name=\"CapabilityWarning\" Margin=\"0,0,0,12\"\n Visibility=\"Collapsed\"\n Background=\"{StaticResource FsLossRecodeBg}\"\n BorderBrush=\"#3DFFC53D\" BorderThickness=\"1\"\n CornerRadius=\"6\" Padding=\"12,10\">\n <Grid>\n <Grid.ColumnDefinitions>\n <ColumnDefinition Width=\"Auto\"/>\n <ColumnDefinition Width=\"*\"/>\n </Grid.ColumnDefinitions>\n <Path Grid.Column=\"0\" Width=\"16\" Height=\"16\" Stretch=\"Uniform\"\n VerticalAlignment=\"Top\" Margin=\"0,1,10,0\"\n Stroke=\"{StaticResource FsLossRecodeText}\" StrokeThickness=\"1.5\"\n Data=\"M12,2 L22,20 L2,20 Z M12,9 L12,14 M12,17 L12,17.01\"/>\n <StackPanel Grid.Column=\"1\">\n <TextBlock x:Name=\"CapabilityWarningText\"\n FontFamily=\"{StaticResource FsFontSans}\" FontSize=\"12\"\n Foreground=\"{StaticResource FsLossRecodeText}\"\n TextWrapping=\"Wrap\"\n Text=\"2개 형식이 외부 도구를 기다립니다\"/>\n <TextBlock x:Name=\"CapabilityWarningLink\" Margin=\"0,6,0,0\"\n FontFamily=\"{StaticResource FsFontSans}\" FontSize=\"11\"\n FontWeight=\"SemiBold\" Cursor=\"Hand\"\n Foreground=\"{StaticResource FsAccentBlue}\"\n Text=\"진단 열기 →\"\n MouseLeftButtonUp=\"OnDiagnoseClick\"/>\n </StackPanel>\n </Grid>\n</Border>"
},
{
"purpose": "MainWindow.xaml.cs — LossClass → 색/라벨/툴팁 매핑 헬퍼 (FormatPalette와 동형, SSOT 한 곳)",
"xaml": "internal static class LossPalette\n{\n // (DotKey, TextKey, BgKey, 한국어 라벨, 툴팁)\n public static (string Dot, string Text, string Bg, string Label, string Tip) For(LossClass loss) => loss switch\n {\n LossClass.Lossless => (\"FsLossLosslessDot\", \"FsLossLosslessText\", \"FsLossLosslessBg\",\n \"무손실\", \"픽셀·내용을 완전 보존합니다\"),\n LossClass.Container => (\"FsLossContainerDot\", \"FsLossContainerText\", \"FsLossContainerBg\",\n \"재포장\", \"컨테이너·구조만 변경, 내용은 보존됩니다\"),\n LossClass.Recode => (\"FsLossRecodeDot\", \"FsLossRecodeText\", \"FsLossRecodeBg\",\n \"재인코딩\", \"재인코딩으로 화질이 일부 손실됩니다\"),\n LossClass.Rasterize => (\"FsLossRasterizeDot\", \"FsLossRasterizeText\", \"FsLossRasterizeBg\",\n \"래스터화\", \"벡터·텍스트 편집성이 사라집니다 (단방향)\"),\n _ => (\"FsLossRecodeDot\", \"FsLossRecodeText\", \"FsLossRecodeBg\", \"재인코딩\", \"\"),\n };\n\n // 배지/pill을 Recode·Rasterize일 때만 노출하는 절제 규칙\n public static Visibility PillVisibility(LossClass loss) =>\n loss is LossClass.Recode or LossClass.Rasterize\n ? Visibility.Visible : Visibility.Collapsed;\n}"
},
{
"purpose": "MainWindow.xaml.cs — 선택한 출력의 worst-case 손실 등급 계산 (큐 전체 입력에 대해 FindBestPath 경로의 최악 등급 중 최댓값). 기존 RefreshAvailableOutputFormats에서 호출",
"xaml": "// 큐 전체에서 이 출력으로 갈 때의 '최악 손실 등급'을 산출 (Max severity)\nprivate LossClass WorstLossFor(string outputExt)\n{\n var graph = ((App)Application.Current).Engine.Providers.Graph;\n var worst = LossClass.Lossless;\n var inputs = _activeQueue.Count == 0\n ? Array.Empty<string>()\n : _activeQueue.Select(q => Path.GetExtension(q.SourcePath)).Distinct().ToArray();\n\n foreach (var input in inputs)\n {\n var path = graph.FindBestPath(input, outputExt, maxHops: 3, allowLossy: true);\n if (path is null) continue;\n foreach (var edge in path) // 경로의 worst edge가 그 변환의 등급\n if ((int)edge.Loss > (int)worst) worst = edge.Loss;\n }\n return worst; // enum 순서가 곧 심각도: Lossless<Container<Recode<Rasterize\n}\n\nprivate void UpdateLossGradeBadge(string? outputExt)\n{\n if (LossGradeBadge is null || outputExt is null) return;\n var grade = WorstLossFor(outputExt);\n var (dot, text, bg, label, tip) = LossPalette.For(grade);\n LossGradeDot.Fill = (Brush)FindResource(dot);\n LossGradeLabel.Foreground = (Brush)FindResource(text);\n LossGradeBadge.Background = (Brush)FindResource(bg);\n LossGradeLabel.Text = grade == LossClass.Recode ? \"재인코딩 (화질 손실)\"\n : grade == LossClass.Rasterize ? \"래스터화 (편집성 상실)\" : label;\n LossGradeBadge.ToolTip = tip;\n // 절제: 무손실/재포장은 배지를 조용히 숨김\n LossGradeBadge.Visibility = LossPalette.PillVisibility(grade);\n}"
}
]
},
{
"key": "polish",
"area": "Everything2Everything (FormatShift Utility) — 전반 UX 폴리시: 빈 큐 상태 / 드롭 피드백 / 미리보기 / 여백 / hover 마이크로인터랙션 + FsFmt 형식색 확장",
"inspiredBy": "Linear (배경 3단 ramp #08090A→#0F1011→#1C1E22 / 사이드바 dimmer 위계 / radius 6px 통일 / status dot 8px+6px gap / Inter letter-spacing 음수 보정 -0.1~-0.3px) · Vercel Geist (보더를 rgba(255,255,255,0.06~0.08) 투명도 기반으로, 그림자 대신 보더로 패널 분리) · Notion Calendar/구 Cron (그리드 보더 9% 알파 / 팝오버 그림자 0 4px 12px rgba(0,0,0,0.45) 최소 사용 / tabular-nums 숫자 정렬) · Raycast (List/Detail 좌우 패턴 / row 내부 패딩을 높이에 비례 / 시맨틱 토큰 추상화 → WPF DynamicResource 궁합) · Things 3 (hover 시에만 액션 버튼 페이드인 150~200ms) · Arc (형식 카테고리별 좌측 2px 저채도 accent bar) · Linear/Notion Empty State 4요소(헤드라인+plain language 보조설명+모노크롬 아이콘+구체적 CTA, 같은 화면에서 인터랙티브하게) · Catppuccin Mocha(신규 형식색 파스텔 — csv/json/xlsx/svg/video/audio) + VS Code Material Icon Theme/Seti UI(형식별 hue 컨벤션: json=골드, 데이터/표=그린, svg=핑크-마젠타, video=코랄, audio=라이트퍼플) · Radix Colors dark step9/step11(status dot solid=step9, 배지 텍스트=step11)",
"proposal": "3컬럼 레이아웃(사이드바 320 / 좌측 큐·이력 / 우측 380 Preview)을 그대로 유지하면서, 체감이 가장 큰 5개 영역만 리서치 기반으로 다듬는다.\n\n(1) 빈 큐 상태(DropZoneEmpty)를 '수동 안내문' → 'Linear/Notion식 인터랙티브 empty state'로 전환. 점선 드롭존 Border(BorderBrush=FsAccentBlue 35% 알파, 12,8 dash, radius 8) + 모노크롬 다운로드 아이콘 + 헤드라인 'Drop files to convert' + plain-language 보조설명 + 실제 동작하는 'Browse files' 버튼(Ctrl+O 연결) + 지원 형식 칩 프리뷰(WrapPanel, 형식색 칩)를 같은 자리에 배치. 현란한 일러스트 금지.\n\n(2) 드롭 피드백(DropHintOverlay)을 평면 스크림 → 액센트 점선 프레임 + 살짝 진해지는 배경(#E6090A0C)으로. 내부에 dashed Border(FsAccentBlue 2px, radius 12, margin 24)를 둬 '여기로 떨어뜨리면 들어간다'는 영역감을 준다.\n\n(3) hover 마이크로인터랙션(Things 3). 큐 row의 삭제 X 아이콘을 평소 Opacity 0 → IsMouseOver 시 1로 페이드인(150ms), row 배경도 hover 시 FsBgSurfaceHover로. Past Results row도 동일. 폴더 열기 아이콘 동일 처리.\n\n(4) 형식 카테고리 accent bar(Arc). 큐/이력 row 좌측에 2px 저채도 세로 바(형식색)를 둬 이미지/문서/표/벡터/영상/오디오를 한눈에 구분.\n\n(5) 신규 6개 형식색 정상화 — 회색(FsFmtOther) 방치된 csv/json/xlsx/svg/video/audio에 Catppuccin Mocha 톤 + 형식 컨벤션 hue를 부여(json=골드 #E3B341, csv=라임 #68A063, xlsx=엑셀그린 #38A169, svg=핑크마젠타 #E05A9C, video=코랄 #FF7043, audio=라이트퍼플 #B794F4). 기존 14색과 충돌 없이 동급 밝기.\n\n(6) 여백/타이포 폴리시: 보더를 rgba 투명도 기반(Geist), Inter letter-spacing 음수 보정, 숫자 컬럼 tabular-nums, radius 6px(컨테이너)/4px(칩) 2단계 수렴.\n\n다크 테마와 FsBg/FsText/FsFmt 토큰 체계는 100% 계승하고, 신규 토큰은 기존 명명 규칙(FsFmtXxx)으로만 확장한다.",
"colorTokens": [
{
"name": "FsFmtJson (신규)",
"hex": "#E3B341",
"usage": "JSON 형식 칩/배지. Material json=#F9A825·Seti yellow 컨벤션 따른 골드. 기존 heic(#D69E2E)와 충돌 회피 위해 살짝 더 노랗게. '데이터=노랑' 직관"
},
{
"name": "FsFmtCsv (신규)",
"hex": "#68A063",
"usage": "CSV 형식 칩/배지. Material table=#8BC34A·Seti green 컨벤션. 데이터테이블 라임그린, webp(#48BB78)와 구분되게 딥하게"
},
{
"name": "FsFmtXlsx (신규)",
"hex": "#38A169",
"usage": "XLSX 형식 칩/배지. 엑셀 브랜드 그린과 정합. csv와 같은 데이터군이되 명도로 구분"
},
{
"name": "FsFmtSvg (신규)",
"hex": "#E05A9C",
"usage": "SVG 벡터 형식 칩/배지. json 골드와 한 화면에서 즉시 구분되도록 핑크-마젠타(Seti svg=purple/css=핑크 계열 차용)"
},
{
"name": "FsFmtVideo (신규)",
"hex": "#FF7043",
"usage": "mp4/webm/mkv/mov/avi 영상 칩/배지. Material video=#FF9800 난색 컨벤션 + pdf 레드(#E53E3E)와 구분되는 코랄/오렌지레드"
},
{
"name": "FsFmtAudio (신규)",
"hex": "#B794F4",
"usage": "mp3/aac/m4a/opus/ogg/flac/wav 오디오 칩/배지. Seti audio=purple 컨벤션 + png/gif 퍼플과 구분되는 라이트 바이올렛"
},
{
"name": "FsFmtMd (신규, 선택)",
"hex": "#60A5FA",
"usage": "Markdown 칩. Material markdown=blue 컨벤션. Tailwind blue-400로 기존 액센트 계열과 통일"
},
{
"name": "FsBorderHairline (신규)",
"hex": "#14FFFFFF",
"usage": "Geist식 8% 투명 보더(rgba 255,255,255,0.08). 3컬럼·row 구분선을 진한 회색 대신 투명도로 — 다크에서 자연스러움. 기존 FsBorderSubtle(#26282D)을 대체하지 않고 '구분선 전용'으로 추가"
},
{
"name": "FsBorderHairlineWeak (신규)",
"hex": "#0FFFFFFF",
"usage": "그리드 라인 6% 투명 보더(Notion Calendar). 이력 row 하단 구분선"
},
{
"name": "FsAccentBlueGlow (신규)",
"hex": "#593B72FF",
"usage": "드롭존/포커스용 액센트 35% 알파(ARGB #59=35%). 점선 드롭존 보더·empty state 강조"
},
{
"name": "FsStatusSuccessBg (신규)",
"hex": "#132D21",
"usage": "Radix Green3 — 'Success' 배지 subtle fill. 기존 인라인 #10B98122 하드코딩을 토큰화"
},
{
"name": "FsStatusSuccessText",
"hex": "#3DD68C",
"usage": "Radix Green11 — Success 배지 텍스트(현 FsAccentGreen #10B981 유지 가능하나 다크 대비는 step11이 더 안전)"
},
{
"name": "FsStatusWarnBg (신규)",
"hex": "#302008",
"usage": "Radix Amber3 — '손실(lossy)' 배지·InfoBar tint 배경"
},
{
"name": "FsStatusWarnText",
"hex": "#FFCA16",
"usage": "Radix Amber11 — 손실/주의 배지 텍스트"
}
],
"xamlSnippets": [
{
"purpose": "FormatShiftTheme.xaml — 신규 형식색 6종 + 마크다운 + Geist 투명 보더 + Radix status 토큰 추가 (기존 28~41행 FsFmt 블록 바로 아래에 삽입). FsFmtOther 폴백을 실제 색으로 승격.",
"xaml": "<!-- New format pills (Catppuccin tone + Material/Seti hue convention) -->\n<SolidColorBrush x:Key=\"FsFmtJson\" Color=\"#E3B341\"/>\n<SolidColorBrush x:Key=\"FsFmtCsv\" Color=\"#68A063\"/>\n<SolidColorBrush x:Key=\"FsFmtXlsx\" Color=\"#38A169\"/>\n<SolidColorBrush x:Key=\"FsFmtSvg\" Color=\"#E05A9C\"/>\n<SolidColorBrush x:Key=\"FsFmtVideo\" Color=\"#FF7043\"/>\n<SolidColorBrush x:Key=\"FsFmtAudio\" Color=\"#B794F4\"/>\n<SolidColorBrush x:Key=\"FsFmtMd\" Color=\"#60A5FA\"/>\n\n<!-- Geist hairline borders (alpha-based, 구분선 전용) -->\n<SolidColorBrush x:Key=\"FsBorderHairline\" Color=\"#14FFFFFF\"/>\n<SolidColorBrush x:Key=\"FsBorderHairlineWeak\" Color=\"#0FFFFFFF\"/>\n<SolidColorBrush x:Key=\"FsAccentBlueGlow\" Color=\"#593B72FF\"/>\n\n<!-- Radix dark status tokens (인라인 하드코딩 #10B98122 토큰화) -->\n<SolidColorBrush x:Key=\"FsStatusSuccessBg\" Color=\"#132D21\"/>\n<SolidColorBrush x:Key=\"FsStatusSuccessText\" Color=\"#3DD68C\"/>\n<SolidColorBrush x:Key=\"FsStatusWarnBg\" Color=\"#302008\"/>\n<SolidColorBrush x:Key=\"FsStatusWarnText\" Color=\"#FFCA16\"/>"
},
{
"purpose": "FormatShiftTheme.xaml — 타이포 letter-spacing 음수 보정(Linear) + tabular-nums(Notion Calendar). 기존 FsBodyStyle/FsLabelStyle/FsMonoStyle Setter에 추가. WPF는 TextBlock에 직접 letter-spacing이 없으므로 TextOptions/Typography 사용.",
"xaml": "<!-- FsBodyStyle 에 추가 -->\n<Setter Property=\"TextOptions.TextFormattingMode\" Value=\"Ideal\"/>\n<Setter Property=\"Typography.Kerning\" Value=\"True\"/>\n\n<!-- 숫자 정렬용 신규 스타일 (파일크기/진행률/해상도) -->\n<Style x:Key=\"FsNumStyle\" TargetType=\"TextBlock\" BasedOn=\"{StaticResource FsMonoStyle}\">\n <Setter Property=\"Typography.NumeralAlignment\" Value=\"Tabular\"/>\n <Setter Property=\"Typography.SlashedZero\" Value=\"True\"/>\n</Style>"
},
{
"purpose": "MainWindow.xaml — 빈 큐 상태(DropZoneEmpty, 363~380행 교체). Linear/Notion식 인터랙티브 empty state: 점선 드롭존 + 모노크롬 아이콘 + 헤드라인 + plain-language + 동작하는 Browse 버튼 + 형식 칩 프리뷰.",
"xaml": "<Grid x:Name=\"DropZoneEmpty\">\n <Border Background=\"{StaticResource FsBgBase}\" Padding=\"48\">\n <Border HorizontalAlignment=\"Center\" VerticalAlignment=\"Center\"\n MaxWidth=\"420\" Padding=\"40,48\" CornerRadius=\"12\"\n Background=\"{StaticResource FsBgPanel}\"\n BorderBrush=\"{StaticResource FsAccentBlueGlow}\" BorderThickness=\"1.5\">\n <Border.Resources>\n <Style TargetType=\"Border\"><Setter Property=\"SnapsToDevicePixels\" Value=\"True\"/></Style>\n </Border.Resources>\n <StackPanel HorizontalAlignment=\"Center\">\n <!-- 모노크롬 다운로드 아이콘 (Linear식, 현란한 일러스트 금지) -->\n <Border Width=\"56\" Height=\"56\" CornerRadius=\"28\"\n Background=\"#143B72FF\" HorizontalAlignment=\"Center\">\n <Path Width=\"26\" Height=\"26\" Stretch=\"Uniform\" HorizontalAlignment=\"Center\" VerticalAlignment=\"Center\"\n Stroke=\"{StaticResource FsAccentBlue}\" StrokeThickness=\"1.8\"\n Data=\"M21,15 L21,19 C21,20.1 20.1,21 19,21 L5,21 C3.9,21 3,20.1 3,19 L3,15 M7,10 L12,15 L17,10 M12,15 L12,3\"/>\n </Border>\n <TextBlock Margin=\"0,20,0,0\" FontSize=\"16\" FontWeight=\"SemiBold\"\n Foreground=\"{StaticResource FsTextPrimary}\" HorizontalAlignment=\"Center\"\n Text=\"Drop files to convert\"/>\n <TextBlock Margin=\"0,6,0,0\" Style=\"{StaticResource FsCaptionStyle}\"\n HorizontalAlignment=\"Center\" TextAlignment=\"Center\" TextWrapping=\"Wrap\"\n Text=\"이미지·문서·표·벡터·영상·오디오를 드래그하거나 아래에서 직접 고르세요. 큐에 넣으면 변환 가능한 형식이 자동 필터됩니다.\"/>\n <!-- 동작하는 CTA (Ctrl+O 와 동일) -->\n <Button Margin=\"0,20,0,0\" Width=\"140\" Content=\"Browse files\"\n Style=\"{StaticResource FsPrimaryButtonStyle}\"\n Command=\"{Binding AddFilesCommand, RelativeSource={RelativeSource AncestorType=Window}}\"/>\n <!-- 지원 형식 칩 프리뷰 -->\n <WrapPanel HorizontalAlignment=\"Center\" Margin=\"0,24,0,0\" MaxWidth=\"320\">\n <WrapPanel.Resources>\n <Style TargetType=\"Border\">\n <Setter Property=\"CornerRadius\" Value=\"4\"/>\n <Setter Property=\"Padding\" Value=\"7,3\"/>\n <Setter Property=\"Margin\" Value=\"3\"/>\n </Style>\n <Style TargetType=\"TextBlock\">\n <Setter Property=\"FontSize\" Value=\"10\"/>\n <Setter Property=\"FontWeight\" Value=\"SemiBold\"/>\n <Setter Property=\"FontFamily\" Value=\"{StaticResource FsFontMono}\"/>\n </Style>\n </WrapPanel.Resources>\n <Border Background=\"#26805AD5\"><TextBlock Text=\"PNG\" Foreground=\"{StaticResource FsFmtPng}\"/></Border>\n <Border Background=\"#263182CE\"><TextBlock Text=\"JPG\" Foreground=\"{StaticResource FsFmtJpg}\"/></Border>\n <Border Background=\"#26E53E3E\"><TextBlock Text=\"PDF\" Foreground=\"{StaticResource FsFmtPdf}\"/></Border>\n <Border Background=\"#262B6CB0\"><TextBlock Text=\"DOCX\" Foreground=\"{StaticResource FsFmtDocx}\"/></Border>\n <Border Background=\"#2638A169\"><TextBlock Text=\"XLSX\" Foreground=\"{StaticResource FsFmtXlsx}\"/></Border>\n <Border Background=\"#26E3B341\"><TextBlock Text=\"JSON\" Foreground=\"{StaticResource FsFmtJson}\"/></Border>\n <Border Background=\"#26E05A9C\"><TextBlock Text=\"SVG\" Foreground=\"{StaticResource FsFmtSvg}\"/></Border>\n <Border Background=\"#26FF7043\"><TextBlock Text=\"MP4\" Foreground=\"{StaticResource FsFmtVideo}\"/></Border>\n <Border Background=\"#26B794F4\"><TextBlock Text=\"MP3\" Foreground=\"{StaticResource FsFmtAudio}\"/></Border>\n </WrapPanel>\n </StackPanel>\n </Border>\n </Border>\n</Grid>"
},
{
"purpose": "MainWindow.xaml — 드롭 오버레이(DropHintOverlay, 717~728행 교체). 평면 스크림 → 액센트 점선 프레임으로 '떨어뜨릴 영역' 명시(Geist/Linear).",
"xaml": "<Border x:Name=\"DropHintOverlay\" Visibility=\"Collapsed\" Grid.ColumnSpan=\"2\"\n Background=\"#E6090A0C\" IsHitTestVisible=\"False\">\n <Border Margin=\"24\" CornerRadius=\"12\"\n BorderBrush=\"{StaticResource FsAccentBlue}\" BorderThickness=\"2\">\n <Border.OpacityMask>\n <DrawingBrush/>\n </Border.OpacityMask>\n <StackPanel HorizontalAlignment=\"Center\" VerticalAlignment=\"Center\">\n <Border Width=\"96\" Height=\"96\" CornerRadius=\"48\" Background=\"#1F3B72FF\">\n <Path Width=\"44\" Height=\"44\" Stretch=\"Uniform\" HorizontalAlignment=\"Center\" VerticalAlignment=\"Center\"\n Stroke=\"{StaticResource FsAccentBlue}\" StrokeThickness=\"2\"\n Data=\"M21,15 L21,19 C21,20.1 20.1,21 19,21 L5,21 C3.9,21 3,20.1 3,19 L3,15 M7,10 L12,15 L17,10 M12,15 L12,3\"/>\n </Border>\n <TextBlock Text=\"Drop to add to queue\" Margin=\"0,20,0,0\"\n FontSize=\"18\" FontWeight=\"SemiBold\"\n Foreground=\"{StaticResource FsTextPrimary}\" HorizontalAlignment=\"Center\"/>\n </StackPanel>\n </Border>\n</Border>"
},
{
"purpose": "FormatShiftTheme.xaml — Things 3식 hover row 스타일(공통). 큐/이력 row Border에 적용. 평소 X아이콘 숨김 + hover 시 배경/아이콘 페이드인. ContentControl 기반 재사용 스타일.",
"xaml": "<!-- 큐/이력 row 컨테이너 공통 스타일 -->\n<Style x:Key=\"FsRowStyle\" TargetType=\"Border\">\n <Setter Property=\"Background\" Value=\"{StaticResource FsBgPanel}\"/>\n <Setter Property=\"BorderBrush\" Value=\"{StaticResource FsBorderHairline}\"/>\n <Setter Property=\"BorderThickness\" Value=\"1\"/>\n <Setter Property=\"CornerRadius\" Value=\"6\"/>\n <Setter Property=\"Padding\" Value=\"12,10\"/>\n <Setter Property=\"Cursor\" Value=\"Hand\"/>\n <Style.Triggers>\n <Trigger Property=\"IsMouseOver\" Value=\"True\">\n <Setter Property=\"Background\" Value=\"{StaticResource FsBgSurfaceHover}\"/>\n <Setter Property=\"BorderBrush\" Value=\"{StaticResource FsBorderSubtle}\"/>\n </Trigger>\n </Style.Triggers>\n</Style>\n\n<!-- hover 시에만 페이드인 되는 액션 아이콘 버튼 (Things 3) -->\n<Style x:Key=\"FsRowActionStyle\" TargetType=\"Button\">\n <Setter Property=\"Background\" Value=\"Transparent\"/>\n <Setter Property=\"BorderThickness\" Value=\"0\"/>\n <Setter Property=\"Padding\" Value=\"6\"/>\n <Setter Property=\"Cursor\" Value=\"Hand\"/>\n <Setter Property=\"Opacity\" Value=\"0\"/>\n <Setter Property=\"Template\">\n <Setter.Value>\n <ControlTemplate TargetType=\"Button\">\n <Border x:Name=\"Bd\" Background=\"{TemplateBinding Background}\"\n CornerRadius=\"4\" Padding=\"{TemplateBinding Padding}\">\n <ContentPresenter HorizontalAlignment=\"Center\" VerticalAlignment=\"Center\"/>\n </Border>\n <ControlTemplate.Triggers>\n <Trigger Property=\"IsMouseOver\" Value=\"True\">\n <Setter TargetName=\"Bd\" Property=\"Background\" Value=\"#14FFFFFF\"/>\n </Trigger>\n </ControlTemplate.Triggers>\n </ControlTemplate>\n </Setter.Value>\n </Setter>\n <!-- 부모 Border(IsMouseOver) hover 시 페이드인: row DataTemplate 안에서 DataTrigger로 -->\n</Style>"
},
{
"purpose": "MainWindow.xaml — 큐 row DataTemplate(386~446행)에 ① Arc식 좌측 형식 accent bar 추가 ② Things3식 X아이콘 hover 페이드인 ③ 숫자 컬럼 tabular-nums. 핵심 변경부만 발췌.",
"xaml": "<Border x:Name=\"RowRoot\" Margin=\"0,0,0,8\" Padding=\"0\"\n Style=\"{StaticResource FsRowStyle}\"\n PreviewMouseLeftButtonUp=\"OnQueueRowClick\" Tag=\"{Binding}\">\n <Grid>\n <Grid.ColumnDefinitions>\n <ColumnDefinition Width=\"2\"/> <!-- Arc accent bar -->\n <ColumnDefinition Width=\"40\"/>\n <ColumnDefinition Width=\"*\"/>\n <ColumnDefinition Width=\"100\"/>\n <ColumnDefinition Width=\"120\"/>\n <ColumnDefinition Width=\"40\"/>\n </Grid.ColumnDefinitions>\n <!-- 좌측 형식 accent bar (저채도, 형식색) -->\n <Border Grid.Column=\"0\" Width=\"2\" CornerRadius=\"1\"\n Background=\"{Binding FormatBrush}\" Opacity=\"0.7\"\n VerticalAlignment=\"Stretch\" Margin=\"0,4\"/>\n <Border Grid.Column=\"1\" Width=\"32\" Height=\"32\" CornerRadius=\"4\" Margin=\"10,0,0,0\"\n Background=\"{Binding FormatBrush}\" VerticalAlignment=\"Center\">\n <TextBlock Text=\"{Binding FormatLabel}\" Foreground=\"White\" FontSize=\"10\"\n FontWeight=\"SemiBold\" HorizontalAlignment=\"Center\" VerticalAlignment=\"Center\"/>\n </Border>\n <!-- ...(파일명/메타/프로그레스 동일)... 크기/상태는 FsNumStyle 로 -->\n <TextBlock Grid.Column=\"3\" Text=\"{Binding SizeText}\" Style=\"{StaticResource FsNumStyle}\"\n HorizontalAlignment=\"Right\" VerticalAlignment=\"Center\"/>\n <!-- hover 시에만 보이는 삭제 버튼 -->\n <Button Grid.Column=\"5\" Style=\"{StaticResource FsRowActionStyle}\"\n Click=\"OnRemoveQueueItem\" Tag=\"{Binding}\">\n <Button.Triggers>\n <EventTrigger RoutedEvent=\"Button.Loaded\"/>\n </Button.Triggers>\n <Path Width=\"14\" Height=\"14\" Stretch=\"Uniform\" Stroke=\"{StaticResource FsTextTertiary}\"\n StrokeThickness=\"2\" Data=\"M18,6 L6,18 M6,6 L18,18\"/>\n </Button>\n </Grid>\n <!-- row hover 시 액션 버튼 페이드인 (DataTrigger: 부모 IsMouseOver) -->\n <Border.Style>\n <Style TargetType=\"Border\" BasedOn=\"{StaticResource FsRowStyle}\">\n <Style.Triggers>\n <Trigger Property=\"IsMouseOver\" Value=\"True\">\n <Setter Property=\"Tag\" Value=\"{Binding}\"/>\n </Trigger>\n </Style.Triggers>\n </Style>\n </Border.Style>\n</Border>"
},
{
"purpose": "MainWindow.xaml — row 액션 버튼 hover 페이드인을 트리거로 연결(위 스니펫의 정석 버전). 부모 Border의 IsMouseOver에 바인딩해 Opacity 0→1 애니메이션(150ms). DataTemplate 내부 Button.Style 에 삽입.",
"xaml": "<Button.Style>\n <Style TargetType=\"Button\" BasedOn=\"{StaticResource FsRowActionStyle}\">\n <Style.Triggers>\n <!-- 부모 row Border 위에 마우스가 있으면 페이드인 -->\n <DataTrigger Binding=\"{Binding IsMouseOver, RelativeSource={RelativeSource AncestorType=Border}}\" Value=\"True\">\n <DataTrigger.EnterActions>\n <BeginStoryboard>\n <Storyboard>\n <DoubleAnimation Storyboard.TargetProperty=\"Opacity\" To=\"1\" Duration=\"0:0:0.15\"/>\n </Storyboard>\n </BeginStoryboard>\n </DataTrigger.EnterActions>\n <DataTrigger.ExitActions>\n <BeginStoryboard>\n <Storyboard>\n <DoubleAnimation Storyboard.TargetProperty=\"Opacity\" To=\"0\" Duration=\"0:0:0.12\"/>\n </Storyboard>\n </BeginStoryboard>\n </DataTrigger.ExitActions>\n </DataTrigger>\n </Style.Triggers>\n </Style>\n</Button.Style>"
},
{
"purpose": "MainWindow.xaml — Preview empty state(PreviewEmpty, 606~618행) 강화. plain-language + '같은 자리 인터랙티브' 원칙으로 'Browse files' 보조 링크 추가, 그리고 Past Results 인라인 #10B98122 → 토큰 교체.",
"xaml": "<!-- PreviewEmpty: 안내문에 행동 단서 추가 -->\n<StackPanel x:Name=\"PreviewEmpty\" HorizontalAlignment=\"Center\" VerticalAlignment=\"Center\" Margin=\"20\">\n <Border Width=\"48\" Height=\"48\" CornerRadius=\"24\" Background=\"#0FFFFFFF\" HorizontalAlignment=\"Center\">\n <Path Width=\"24\" Height=\"24\" Stretch=\"Uniform\" HorizontalAlignment=\"Center\" VerticalAlignment=\"Center\"\n Stroke=\"{StaticResource FsTextTertiary}\" StrokeThickness=\"1.5\"\n Data=\"M1,12 C1,12 5,4 12,4 C19,4 23,12 23,12 C23,12 19,20 12,20 C5,20 1,12 1,12 Z M12,9 A3,3 0 1,1 12,15 A3,3 0 1,1 12,9 Z\"/>\n </Border>\n <TextBlock Margin=\"0,14,0,0\" FontSize=\"13\" FontWeight=\"SemiBold\"\n Foreground=\"{StaticResource FsTextSecondary}\" HorizontalAlignment=\"Center\" Text=\"No selection\"/>\n <TextBlock Margin=\"0,4,0,0\" Style=\"{StaticResource FsCaptionStyle}\"\n HorizontalAlignment=\"Center\" TextAlignment=\"Center\" TextWrapping=\"Wrap\" MaxWidth=\"240\"\n Text=\"큐 또는 이력 항목을 클릭하면 여기에 미리보기와 메타데이터가 표시됩니다.\"/>\n</StackPanel>\n\n<!-- Past Results 날짜 헤더 savings 배지: 하드코딩 #10B98122 → 토큰 (471행) -->\n<Border Grid.Column=\"1\" CornerRadius=\"20\" Padding=\"10,4\"\n Background=\"{StaticResource FsStatusSuccessBg}\">\n <TextBlock Text=\"{Binding SessionSavingsText}\" Style=\"{StaticResource FsNumStyle}\"\n Foreground=\"{StaticResource FsStatusSuccessText}\"/>\n</Border>"
},
{
"purpose": "MainWindow.xaml.cs — 신규 형식색 매핑 연결. FormatPalette.For 와 AllFormats 의 FsFmtOther 폴백을 신규 토큰으로 교체.",
"xaml": "// FormatPalette.For() switch 에 추가\n\"csv\" => (\"CSV\", \"FsFmtCsv\"),\n\"json\" => (\"JSON\", \"FsFmtJson\"),\n\"xlsx\" or \"xls\" => (\"XLSX\", \"FsFmtXlsx\"),\n\"svg\" => (\"SVG\", \"FsFmtSvg\"),\n\"mp4\" or \"webm\" or \"mkv\" or \"mov\" or \"avi\" => (ext.ToUpperInvariant(), \"FsFmtVideo\"),\n\"mp3\" or \"aac\" or \"m4a\" or \"opus\" or \"ogg\" or \"flac\" or \"wav\" => (ext.ToUpperInvariant(), \"FsFmtAudio\"),\n\"md\" or \"markdown\" => (\"MD\", \"FsFmtMd\"),\n\n// AllFormats 배열의 ColorResource 도 동일하게 (csv→FsFmtCsv, json→FsFmtJson, xlsx→FsFmtXlsx,\n// svg→FsFmtSvg, mp4/webm/mkv/mov/avi→FsFmtVideo, mp3/aac/m4a/opus/ogg/flac/wav→FsFmtAudio, md→FsFmtMd)"
}
],
"implementationSteps": [
"1) FormatShiftTheme.xaml 28~41행 FsFmt 블록 아래에 신규 형식색 7종(FsFmtJson/Csv/Xlsx/Svg/Video/Audio/Md) + Geist 투명보더 3종(FsBorderHairline/HairlineWeak/AccentBlueGlow) + Radix status 4종(FsStatusSuccessBg/Text/WarnBg/WarnText)을 추가 (1번 스니펫).",
"2) 같은 파일에 FsNumStyle(tabular-nums) 추가하고 FsBodyStyle에 Typography.Kerning 보정 (2번 스니펫).",
"3) FormatShiftTheme.xaml에 공통 row 스타일 FsRowStyle + 액션버튼 FsRowActionStyle 추가 (5번 스니펫).",
"4) MainWindow.xaml.cs의 FormatPalette.For() switch와 AllFormats 배열에서 csv/json/xlsx/svg/video/audio/md의 ColorResource를 FsFmtOther → 신규 토큰으로 교체 (9번 스니펫). 이걸로 큐/이력/배지 색이 일괄 정상화됨.",
"5) MainWindow.xaml DropZoneEmpty(363~380행)를 인터랙티브 empty state로 교체 — 점선 드롭존 + Browse 버튼(AddFilesCommand 바인딩) + 형식 칩 프리뷰 (3번 스니펫).",
"6) DropHintOverlay(717~728행)를 액센트 점선 프레임 + 진해진 배경(#E6090A0C)으로 교체 (4번 스니펫).",
"7) 큐 row DataTemplate(386~446행)에 좌측 2px accent bar 추가 + 삭제버튼을 FsRowActionStyle로 + 부모 hover DataTrigger로 페이드인(150ms) + SizeText에 FsNumStyle 적용 (6·7번 스니펫). Past Results row(484~560행)도 폴더열기 아이콘에 동일 hover 패턴 적용.",
"8) Past Results 날짜 헤더 savings 배지의 하드코딩 #10B98122 → FsStatusSuccessBg 토큰으로, 텍스트는 FsStatusSuccessText로 교체 (8번 스니펫). PreviewEmpty도 아이콘 원형 칩 + plain-language로 다듬기.",
"9) 여백 통일 패스: 패널 padding 16~24, 그룹 간 24~32(현행 유지), row 내부 12/10, 칩 간격 6~8px로 8 배수 그리드 점검. radius는 컨테이너 6 / 칩·배지 4로 수렴 (대부분 이미 일치).",
"10) dotnet publish 후 카스케이드 재등록 → 사용자가 직접 push & GUI 검증 (MEMORY: project_build_pipeline / feedback_user_workflow)."
],
"userDecisions": [
"신규 형식색을 Catppuccin/Material 컨벤션 hue(제안값: json=#E3B341 골드, csv=#68A063, xlsx=#38A169, svg=#E05A9C 핑크, video=#FF7043 코랄, audio=#B794F4 퍼플)로 갈지, 아니면 기존 14색이 Chakra/Tailwind 톤이므로 Tailwind 400 세트로 통일할지 — 제안은 컨벤션 hue + 기존 톤에 채도 맞춤(전자). 승인 필요.",
"markdown(.md) 색을 신규 추가(FsFmtMd #60A5FA 블루)할지, 기존대로 FsFmtOther 회색 유지할지. (md=blue는 Material 컨벤션이나 docx/jpg 블루와 인접 — 선택 사항)",
"row 좌측 Arc식 2px accent bar를 형식별 '개별 색'으로 둘지, 아니면 이미지/문서/표/벡터/영상/오디오 '카테고리 6색'으로 묶을지. 제안은 형식색 그대로(개별). 카테고리로 묶으면 더 차분하지만 코드에 카테고리 매핑 추가 필요.",
"드롭 오버레이 점선(dashed) 표현 — WPF Border는 native dash가 없어 StrokeDashArray를 쓰려면 Rectangle 오버레이로 대체해야 함. 제안 스니펫은 solid 2px 프레임으로 단순화했는데, 진짜 점선이 필요하면 Rectangle+StrokeDashArray=\"6,4\"로 교체할지 확정 필요.",
"Things3 hover 페이드인 애니메이션(150ms)을 전 row에 적용 시 대량 큐(수백 개)에서 Storyboard 비용 — 큐가 클 때 VirtualizingStackPanel 적용 여부도 같이 볼지(현재 ItemsControl은 가상화 비활성)."
]
}
]