diff --git a/docs/ssot/_data/design_designs.json b/docs/ssot/_data/design_designs.json new file mode 100644 index 0000000..31ba73b --- /dev/null +++ b/docs/ssot/_data/design_designs.json @@ -0,0 +1,450 @@ +[ + { + "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": "\n\n\n\n\n\n\n\n\n\n\n\n" + }, + { + "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": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + }, + { + "purpose": "PasswordBox 전용 입력 스타일 (FsPathInputStyle을 PasswordBox용으로 포팅 — PART_ContentHost 동일 사용 가능)", + "xaml": "" + }, + { + "purpose": "SettingsWindow.xaml — 창 셸 + API 키 한 행(PasswordBox + 👁 토글 + Verify + status dot 배지). Cursor/Zed/Raycast 패턴 집약", + "xaml": "\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n " + } + ], + "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 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": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + }, + { + "purpose": "FormatShiftTheme.xaml — 재사용 dot + pill 스타일 (Linear 8px dot / shadcn pill 22px h, 6px radius)", + "xaml": "\n\n\n\n" + }, + { + "purpose": "MainWindow.xaml — 출력 드롭다운 아래 '현재 선택 손실 등급' pill (OutputFormatBadge 영역 line 89~101 아래, OutputFormatHint 위에 삽입). dot 8px + 6px gap + 한국어 라벨", + "xaml": "\n\n \n \n \n \n" + }, + { + "purpose": "MainWindow.xaml — 큐 row ItemTemplate: 형식 배지(line 401~409) 우상단에 손실 dot 오버레이. Lossless/Container는 dot만, Recode/Rasterize는 dot이 눈에 띄는 색", + "xaml": "\n\n \n \n \n \n" + }, + { + "purpose": "MainWindow.xaml — 큐 row의 StateText 컬럼(Grid.Column 3, line 428~432) 위/옆에 조건부 Recode·Rasterize pill (절제: Recode/Rasterize일 때만 Visible)", + "xaml": "\n \n \n \n \n \n \n \n" + }, + { + "purpose": "MainWindow.xaml — capability 경고를 평문 TextBlock(line 233)에서 Radix Amber InfoBar(Fluent Severity=Warning)로 교체. subtle tint 배경 + 단일 Diagnose CTA", + "xaml": "\n \n \n \n \n \n \n \n \n \n \n \n" + }, + { + "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()\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\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + }, + { + "purpose": "FormatShiftTheme.xaml — 타이포 letter-spacing 음수 보정(Linear) + tabular-nums(Notion Calendar). 기존 FsBodyStyle/FsLabelStyle/FsMonoStyle Setter에 추가. WPF는 TextBlock에 직접 letter-spacing이 없으므로 TextOptions/Typography 사용.", + "xaml": "\n\n\n\n\n" + }, + { + "purpose": "MainWindow.xaml — 빈 큐 상태(DropZoneEmpty, 363~380행 교체). Linear/Notion식 인터랙티브 empty state: 점선 드롭존 + 모노크롬 아이콘 + 헤드라인 + plain-language + 동작하는 Browse 버튼 + 형식 칩 프리뷰.", + "xaml": "\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n" + }, + { + "purpose": "MainWindow.xaml — row 액션 버튼 hover 페이드인을 트리거로 연결(위 스니펫의 정석 버전). 부모 Border의 IsMouseOver에 바인딩해 Opacity 0→1 애니메이션(150ms). DataTemplate 내부 Button.Style 에 삽입.", + "xaml": "\n \n" + }, + { + "purpose": "MainWindow.xaml — Preview empty state(PreviewEmpty, 606~618행) 강화. plain-language + '같은 자리 인터랙티브' 원칙으로 'Browse files' 보조 링크 추가, 그리고 Past Results 인라인 #10B98122 → 토큰 교체.", + "xaml": "\n\n \n \n \n \n \n\n\n\n\n \n" + }, + { + "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은 가상화 비활성)." + ] + } +] \ No newline at end of file diff --git a/docs/ssot/_data/design_research.json b/docs/ssot/_data/design_research.json new file mode 100644 index 0000000..3ea5b2e --- /dev/null +++ b/docs/ssot/_data/design_research.json @@ -0,0 +1,504 @@ +[ + { + "key": "palettes", + "topic": "다크 UI 색상 시스템 리서치 — FormatShift Utility(다크 Fluent 파일 변환 앱)의 형식별 카테고리 컬러 팔레트 선정", + "findings": [ + { + "system": "Radix Colors (dark scales)", + "url": "https://www.radix-ui.com/colors/docs/palette-composition/understanding-the-scale", + "takeaway": "12단계 스케일을 '의미 역할'로 표준화한 게 핵심. step1=앱 배경 / step2=서브틀 배경 / step3-5=컴포넌트 배경(normal/hover/pressed) / step6-8=보더 / step9=가장 채도 높은 솔리드(브랜드/뱃지 칠) / step10=hover / step11=저대비 텍스트 / step12=고대비 텍스트. 형식별 색을 '텍스트/라벨'로 쓸 거면 step11, '꽉 찬 칩 배경'으로 쓸 거면 step9를 골라야 대비가 보장됨. 현재 프로젝트의 FsBg(090A0C~1C1E22)/FsText 3계층 구조가 Radix step1·2·3·11·12와 1:1 매핑되므로 이 철학을 그대로 차용하면 토큰 정리가 깔끔해짐." + }, + { + "system": "Catppuccin (Mocha)", + "url": "https://catppuccin.com/palette/", + "takeaway": "다크 배경(base #1e1e2e)에서 명도가 거의 균일하게 튜닝된 14개 파스텔 액센트가 그대로 '12색 카테고리 팔레트'로 최적. 서로 충돌 안 하고 채도/명도가 고름 — 이 앱의 신규 형식 색 분배에 1순위 추천. 배경 3계층(crust #11111b / mantle #181825 / base #1e1e2e)·텍스트 3계층(text #cdd6f4 / subtext0 #a6adc8 / overlay1 #7f849c)도 현 구조와 정확히 대응." + }, + { + "system": "Tokyo Night", + "url": "https://github.com/folke/tokyonight.nvim", + "takeaway": "배경 #1a1b26 / 사이드바·탭 #1f2335 / 보더 #3b4261 라는 '약간 푸른빛 도는 다크' 계층이 현재 앱의 중립 회색 계층보다 따뜻함은 덜하지만 액센트 8색(빨강 f7768e·초록 9ece6a·노랑 e0af68·파랑 7aa2f7·마젠타 bb9af7·시안 7dcfff)이 채도가 적당히 죽어 있어 다수 칩을 나란히 둬도 눈이 안 아픔. Catppuccin보다 살짝 더 채도가 있는 톤을 원하면 대안." + }, + { + "system": "Tailwind CSS", + "url": "https://tailwindcss.com/docs/colors", + "takeaway": "slate/zinc 중립 + 채도 컬러를 '셰이드 번호'로 균일 관리하는 방식이 표준. 다크 배경에선 500이 살짝 어두워 400~500을 텍스트/라벨용으로 쓰는 게 정석(red-400 #F87171, green-400 #4ADE80, blue-400 #60A5FA, yellow-400 #FACC15). 현재 앱의 FsAccentGreen #10B981·Amber #F59E0B·Red #EF4444가 이미 Tailwind 값이므로, 신규 형식색도 Tailwind 400/500에서 뽑으면 기존 액센트와 일관됨. 단 한 팔레트에서 14색을 뽑으면 인접 색(emerald/teal/cyan 등)이 헷갈릴 수 있어 색상환을 넓게 띄워 골라야 함." + }, + { + "system": "Nord", + "url": "https://www.nordtheme.com/docs/colors-and-palettes", + "takeaway": "Aurora 액센트가 단 5색(빨강 bf616a·주황 d08770·노랑 ebcb8b·초록 a3be8c·보라 b48ead)이라 12 카테고리엔 부족. 저채도·차분함이 강점이지만 색 가짓수가 적어 '많은 형식 구분'엔 부적합 — 참고용. Polar Night(2e3440~4c566a)는 이 앱보다 밝은 회색이라 #090A0C 기조와는 안 맞음." + }, + { + "system": "GitHub Primer (dark)", + "url": "https://primer.style/foundations/color/overview/", + "takeaway": "기능색을 역할명(fg/bg/accent/danger/success/attention/done/sponsors)으로 토큰화. 다크 기본값: 배경 canvas #0d1117·텍스트 fg.default #e6edf3·muted #7d8590·accent #2f81f7. 형식 구분용으로 쓸 만한 '밝은 다크용' 액센트는 syntax 팔레트가 검증됨: 빨강 #ff7b72·주황 #ffa657·노랑/금 (number)#79c0ff·초록 #7ee787·파랑 #79c0ff·연파랑 #a5d6ff·보라 #d2a8ff·danger #f85149. 배경 #0d1117이 이 앱 #090A0C와 가장 가까워 대비 수치를 거의 그대로 신뢰 가능." + } + ], + "recommendation": "결론: 신규 형식(csv/json/xlsx/svg/영상/오디오)과 회색으로 방치된 형식들을 위해 '단일 출처에서 명도·채도가 균일하게 튜닝된 12~14색 세트'를 통째로 채택하라. 현재 FsFmt* 14색은 Chakra UI 계열을 손으로 모은 것이라 명도가 들쭉날쭉하고(예: Docx #2B6CB0·Hwp #9B2C2C·Bmp #718096은 #090A0C 배경에서 너무 어두워 라벨이 안 읽힘) 일관성이 없다. 이를 Catppuccin Mocha 액센트(다크 검증 완료, 1순위) 또는 Radix dark step-11(라벨/텍스트로 쓸 때 WCAG 대비 보장, 2순위)로 일괄 교체할 것을 권한다.\\n\\n구체 적용: (1) 형식색을 '라벨/아이콘 텍스트'로 쓰므로 채도 솔리드(Radix step9)가 아니라 명도가 높은 파스텔(Catppuccin) 또는 step11을 써야 #090A0C에서 4.5:1 이상 대비가 난다. (2) 색상환을 빨강→주황→노랑→초록→틸→시안→파랑→인디고→보라→마젠타→핑크 순으로 넓게 띄워 배정해 인접 형식이 헷갈리지 않게 한다(예: PDF=빨강, XLSX=초록, CSV=틸, JSON=노랑, SVG=주황, 영상=보라, 오디오=핑크, DOCX=파랑). (3) 기존 액센트 FsAccentGreen #10B981·Amber #F59E0B·Red #EF4444가 이미 Tailwind 값이므로, 만약 Catppuccin으로 안 가고 기존과의 통일을 우선한다면 Tailwind 400 세트로 형식색을 뽑아 액센트와 한 가족으로 묶는 것도 정합적이다. (4) 배경/텍스트 3계층은 현 구조가 이미 Radix·Catppuccin 철학과 일치하므로 유지하되, FsBorderSubtle/Strong을 Radix step6/8 개념으로만 재확인하면 충분하다.\\n\\npalette 항목에는 '형식 카테고리 12색'을 Catppuccin Mocha 기준으로 제시한다(다크 배경 대비·조화가 가장 안전). Tailwind/Radix 대체 값은 findings·sources 참고.", + "palette": [ + { + "name": "Red (PDF/HWP 류)", + "hex": "#F38BA8", + "category": "형식 카테고리 (Catppuccin Mocha)" + }, + { + "name": "Peach/Orange (HTML/SVG 류)", + "hex": "#FAB387", + "category": "형식 카테고리 (Catppuccin Mocha)" + }, + { + "name": "Yellow (JSON/HEIC 류)", + "hex": "#F9E2AF", + "category": "형식 카테고리 (Catppuccin Mocha)" + }, + { + "name": "Green (XLSX/WEBP 류)", + "hex": "#A6E3A1", + "category": "형식 카테고리 (Catppuccin Mocha)" + }, + { + "name": "Teal (CSV/RAW 류)", + "hex": "#94E2D5", + "category": "형식 카테고리 (Catppuccin Mocha)" + }, + { + "name": "Sky/Cyan (TIFF 류)", + "hex": "#89DCEB", + "category": "형식 카테고리 (Catppuccin Mocha)" + }, + { + "name": "Sapphire (오디오 류)", + "hex": "#74C7EC", + "category": "형식 카테고리 (Catppuccin Mocha)" + }, + { + "name": "Blue (JPG/DOCX 류)", + "hex": "#89B4FA", + "category": "형식 카테고리 (Catppuccin Mocha)" + }, + { + "name": "Lavender (BMP/Other 류)", + "hex": "#B4BEFE", + "category": "형식 카테고리 (Catppuccin Mocha)" + }, + { + "name": "Mauve (PNG/GIF/영상 류)", + "hex": "#CBA6F7", + "category": "형식 카테고리 (Catppuccin Mocha)" + }, + { + "name": "Pink (AVIF 류)", + "hex": "#F5C2E7", + "category": "형식 카테고리 (Catppuccin Mocha)" + }, + { + "name": "Maroon (예비/위험 형식)", + "hex": "#EBA0AC", + "category": "형식 카테고리 (Catppuccin Mocha)" + }, + { + "name": "배경 Base (앱 최하층, 현 #090A0C 대응)", + "hex": "#11111B", + "category": "배경 계층 (Catppuccin Crust)" + }, + { + "name": "배경 Panel (사이드바/패널)", + "hex": "#181825", + "category": "배경 계층 (Catppuccin Mantle)" + }, + { + "name": "배경 Surface (카드/큐 row)", + "hex": "#1E1E2E", + "category": "배경 계층 (Catppuccin Base)" + }, + { + "name": "텍스트 Primary", + "hex": "#CDD6F4", + "category": "텍스트 계층 (Catppuccin Text)" + }, + { + "name": "텍스트 Secondary", + "hex": "#A6ADC8", + "category": "텍스트 계층 (Catppuccin Subtext0)" + }, + { + "name": "텍스트 Tertiary", + "hex": "#7F849C", + "category": "텍스트 계층 (Catppuccin Overlay1)" + }, + { + "name": "액센트 Blue (대안: Radix blue step9)", + "hex": "#0090FF", + "category": "액센트 (Radix dark)" + }, + { + "name": "형식색 대안 — Tailwind red-400", + "hex": "#F87171", + "category": "형식 대안 (Tailwind 400, 기존 액센트와 동일 계열)" + }, + { + "name": "형식색 대안 — Tailwind green-400", + "hex": "#4ADE80", + "category": "형식 대안 (Tailwind 400)" + }, + { + "name": "형식색 대안 — Tailwind blue-400", + "hex": "#60A5FA", + "category": "형식 대안 (Tailwind 400)" + }, + { + "name": "형식색 대안 — Radix amber step11", + "hex": "#FFCA16", + "category": "형식 대안 (Radix dark step11, 텍스트 대비 보장)" + }, + { + "name": "형식색 대안 — Radix teal step11", + "hex": "#0BD8B6", + "category": "형식 대안 (Radix dark step11)" + } + ], + "sources": [ + "https://www.radix-ui.com/colors/docs/palette-composition/understanding-the-scale", + "https://www.radix-ui.com/colors/docs/palette-composition/scales", + "https://github.com/radix-ui/colors", + "https://catppuccin.com/palette/", + "https://github.com/catppuccin/palette", + "https://github.com/folke/tokyonight.nvim", + "https://github.com/tokyo-night/tokyo-night-vscode-theme", + "https://tailwindcss.com/docs/colors", + "https://www.nordtheme.com/docs/colors-and-palettes", + "https://primer.style/foundations/color/overview/", + "https://github.com/primer/github-vscode-theme", + "https://stylishthemes.github.io/GitHub-Dark/" + ] + }, + { + "key": "ui-patterns", + "topic": "생산성/유틸리티 데스크톱 앱(Linear, Raycast, Geist/Vercel, Arc, Things, Notion Calendar) 다크 모드 UI 패턴 — 3컬럼 파일 변환 앱(FormatShift) 완성도 향상을 위한 리서치", + "findings": [ + { + "system": "Linear (앱 + 디자인 시스템)", + "takeaway": "중성 색상 거의 100%로 구성하고 브랜드 색은 액센트에서만 사용. 배경 ramp가 핵심: Pitch Black #08090A(앱 배경) → Graphite #0F1011(패널/사이드바) → Charcoal #23252A(카드/입력) → Gunmetal #383B3F(보더/구분선). 텍스트는 #F7F8F8(Porcelain, 주) / #D0D6E0(Light Steel, 보조) / #8A8F98(Storm Cloud, 약). 액센트 Aether Blue #5E6AD2. 2024 리디자인의 핵심 교훈: '사이드바를 몇 단계 더 어둡게(dimmer)' 만들어 중앙 작업 영역이 우선순위를 갖게 함 → 현재 FormatShift 좌측 옵션패널/우측 미리보기를 배경보다 약간 어둡게, 중앙 변환큐를 가장 밝게 처리하면 시선 위계가 잡힌다. 구분선(separator)을 줄여 시각 노이즈 감소. radius 6px 통일.", + "url": "https://linear.app/now/how-we-redesigned-the-linear-ui" + }, + { + "system": "Linear 타이포 + spacing", + "takeaway": "Inter Variable 사용 + letter-spacing 음수 보정(display -0.22px, body -0.11px)이 '깔끔한 느낌'의 핵심. 기본 element gap 8px, primary 버튼/카드/입력 radius 6px 통일. FormatShift도 Inter 이미 사용 중이므로 letter-spacing 음수값(-0.1~-0.2px)을 본문/제목에 적용하면 즉각 고급감 상승. LCH/OKLCH 색공간으로 라이트니스 균일화 — 14개 형식색상을 OKLCH 기준 동일 lightness로 재정렬하면 신규 형식(csv/json/xlsx 등)도 자연스럽게 채도만 다른 동급 칩으로 보인다.", + "url": "https://rogerwong.me/2024/11/how-we-redesigned-the-linear-ui-part-ii" + }, + { + "system": "Vercel Geist 디자인 시스템", + "takeaway": "다크 배경 2단계 분리 원칙: Background 1 = #0A0A0A(주 배경, 그 위에 색을 올릴 때), Background 2 = #000(미묘한 차별화가 필요할 때만 절제 사용). 보더는 rgba 8% 불투명도(rgba(255,255,255,0.08))로 매우 낮게 — 다크에서 보더는 '거의 안 보일 만큼' 얇고 투명해야 자연스럽다. radius 0~4px로 매우 보수적, 그림자 거의 안 씀(보더 기반). letter-spacing -0.04em. FormatShift의 3컬럼 구분을 그림자 대신 1px / 8% 보더로 처리하면 다크에서 깔끔. 현재 보더가 진하면 8% 투명도로 낮춰라.", + "url": "https://vercel.com/geist/colors" + }, + { + "system": "Raycast", + "takeaway": "List / Grid / Detail 3종 컴포넌트 = 사실상 좌(목록)·우(상세) 패턴의 정석. Grid의 insets로 아이템 콘텐츠와 보더 사이 여백을 itemSize에 비례시킴 → FormatShift 변환큐 row의 내부 패딩을 row 높이에 비례시키면 밀도 조절 시 일관성 유지. 시맨틱 토큰(background/primaryText/support)을 역할 기반으로 정의해 라이트/다크 자동 적응 — 색을 hex 직접 박지 말고 'surface/text-primary/accent' 같은 역할 이름의 리소스로 추상화하라(WPF DynamicResource와 궁합 좋음).", + "url": "https://developers.raycast.com/api-reference/user-interface" + }, + { + "system": "Notion Calendar (구 Cron)", + "takeaway": "구체 수치가 가장 풍부: radius 4px(sm)/6px(md), 그리드 보더 1px @ 9% 불투명도(구조용, 장식 아님), 팝오버 그림자 0 4px 12px rgba(0,0,0,0.12)로 최소한만. 타이포 극단 대비(display 64px vs label 12px = 5.3:1 비율, 일반 2~3:1보다 큼)로 단일 폰트만으로 포스터 같은 임팩트. 시간 거터는 11px monospaced numerals — FormatShift의 파일크기/진행률 숫자에 JetBrains Mono tabular-nums 적용하면 정렬감 상승. 키보드-퍼스트가 시각적 미니멀리즘을 정당화 → 모든 액션에 단축키 부여하면 버튼/툴바를 줄여도 됨. 밀도는 Zoom In/Out으로 사용자가 조절.", + "url": "https://blakecrosley.com/guides/design/notion-calendar" + }, + { + "system": "Arc Browser", + "takeaway": "Space마다 고유 그라데이션으로 즉각적 시각 인식 부여 + 채도/그레인 조절. 자동 숨김 사이드바로 콘텐츠 우선. soft rounded corners + subtle animation + smooth transition으로 '산만함 제한'이 모토. FormatShift에 적용: 변환 형식(이미지/문서/표/벡터/영상/오디오) 카테고리별로 은은한 그라데이션/색조를 옵션패널 헤더나 큐 row 좌측 accent bar에 부여하면 형식 분류가 직관적. 단, 채도는 낮게(다크 배경에서 grain/저채도가 자연스러움).", + "url": "https://blakecrosley.com/guides/design/arc" + }, + { + "system": "Things 3", + "takeaway": "Apple Design Award 수상의 핵심은 마이크로인터랙션: hover 시에만 액션 버튼이 페이드인(평소엔 숨김)으로 노이즈 제거. hover state는 '무엇이 인터랙션 가능한지'를 알려주는 신호 — row/카드/입력/필터드롭다운 모든 요소에 일관된 hover 피드백 필요. FormatShift 큐 row도 hover 시에만 삭제/재시도/폴더열기 아이콘을 페이드인하면 평소엔 깔끔하고 필요할 때만 노출.", + "url": "https://culturedcode.com/things/features/" + }, + { + "system": "Empty State 베스트 프랙티스 (Linear/Notion 사례)", + "takeaway": "Linear/Notion은 simple monochrome 일러스트로 인터페이스에 녹아들면서 따뜻함 제공(현란한 일러스트 금지). 구성 4요소: 간결한 헤드라인 + 보조 설명(왜 비었는지를 plain language로) + 모노크롬 아이콘/일러스트 + 구체적 CTA. CTA는 'Add Your First Task'처럼 행동 지향 + 구체적. 최신 트렌드는 '같은 화면에서 바로 추가'(passive 메시지 → interactive). FormatShift 빈 변환큐: 'Drop files here to convert' + 점선 드롭존 + 'Browse files' 버튼 + 지원 형식 칩 미리보기를 같은 자리에 인터랙티브하게.", + "url": "https://www.eleken.co/blog-posts/empty-state-ux" + } + ], + "recommendation": "FormatShift에 적용할 구체 변경안 (다크 Fluent 유지 전제):\n\n1) 배경 3단계 위계 재정렬 (Linear 패턴). 현재 #090A0C~#1C1E22를 명확한 역할로 분리: 앱 최외곽/창 배경 #090A0C → 좌측 옵션패널·우측 미리보기 패널 #0F1011~#121316(약간 더 어둡게, dimmer) → 중앙 변환큐 카드/큐 row #1A1C20~#1C1E22(가장 밝게). 핵심: 작업 대상인 중앙 큐가 가장 밝아야 시선이 모인다.\n\n2) 보더는 투명도 기반으로 (Geist + Notion Calendar). 3컬럼 구분과 row 구분을 진한 회색 선 대신 1px rgba(255,255,255,0.06~0.09)로. 그림자는 팝오버/컨텍스트메뉴에만 0 4px 12px rgba(0,0,0,0.4~0.5) 수준으로 최소 사용. 패널 분리는 보더로, 떠 있는 요소만 그림자로.\n\n3) radius 통일 6px (Linear). 버튼/입력/카드/큐 row 모두 6px. 미세 요소(칩/배지)는 4px. 현재 값이 제각각이면 6px/4px 2단계로 수렴.\n\n4) 타이포 letter-spacing 음수 보정. Inter 본문/라벨에 -0.1~-0.2px, 큰 제목엔 -0.3px 수준. 파일크기·진행률·해상도 숫자는 JetBrains Mono + tabular-nums(고정폭 숫자)로 큐에서 세로 정렬.\n\n5) 신규 형식 색상 정상화 (Linear OKLCH 균일화). 회색으로 방치된 csv/json/xlsx/svg/영상/오디오에 기존 14색과 동일 lightness·채도의 색 부여. 권장: 데이터/표 계열 green #0F7B6C(엑셀 연상), 코드/구조 계열 amber #CB912F(json/yaml), 벡터 purple #9065B0, 영상 red/orange, 오디오 pink #C14C8A — 모두 OKLCH lightness 동일하게 맞춰 동급 칩으로 보이게. 칩 스타일은 저채도 배경(색 @ 12~15% 알파) + 진한 텍스트.\n\n6) hover 마이크로인터랙션 (Things 3). 큐 row의 삭제/재시도/출력폴더열기 아이콘은 평소 숨기고 hover 시 페이드인(150~200ms). focus 링은 액센트 #3B72FF @ 2px outline. 모든 인터랙티브 요소(버튼/row/드롭다운)에 일관된 hover 배경(흰색 4~6% 오버레이).\n\n7) 빈 변환큐 empty state. 점선 드롭존 + 모노크롬 아이콘 + 'Drop files to convert, or Browse' + 지원형식 칩 프리뷰를 같은 영역에 인터랙티브하게. 현란한 일러스트 금지(Linear식 모노크롬).\n\n8) 8px spacing scale. 패널 패딩 16px, 그룹 간 24px, row 내부 8/12px, 칩 간격 8px로 8의 배수 그리드 통일.\n\n9) 형식 카테고리 accent bar (Arc). 큐 row 좌측에 형식 카테고리별 2px 저채도 accent bar를 두면 이미지/문서/표/벡터/영상/오디오가 한눈에 구분된다.", + "palette": [ + { + "name": "App Background (창 배경)", + "hex": "#08090A", + "category": "background" + }, + { + "name": "Panel / Sidebar (좌·우 패널, dimmer)", + "hex": "#0F1011", + "category": "background" + }, + { + "name": "Card / Queue Row (중앙 큐, 밝게)", + "hex": "#1C1E22", + "category": "background" + }, + { + "name": "Elevated Surface (입력/호버 표면)", + "hex": "#23252A", + "category": "surface" + }, + { + "name": "Border / Separator (8% 알파)", + "hex": "rgba(255,255,255,0.08)", + "category": "border" + }, + { + "name": "Border subtle (그리드 라인 6%)", + "hex": "rgba(255,255,255,0.06)", + "category": "border" + }, + { + "name": "Text Primary", + "hex": "#F7F8F8", + "category": "text" + }, + { + "name": "Text Secondary", + "hex": "#D0D6E0", + "category": "text" + }, + { + "name": "Text Tertiary / Muted", + "hex": "#8A8F98", + "category": "text" + }, + { + "name": "Accent Blue (현 유지 가능)", + "hex": "#3B72FF", + "category": "accent" + }, + { + "name": "Accent Blue (Linear 대안)", + "hex": "#5E6AD2", + "category": "accent" + }, + { + "name": "Format - Data/Table (xlsx/csv)", + "hex": "#0F7B6C", + "category": "format" + }, + { + "name": "Format - Code/Structure (json/yaml)", + "hex": "#CB912F", + "category": "format" + }, + { + "name": "Format - Vector (svg)", + "hex": "#9065B0", + "category": "format" + }, + { + "name": "Format - Video", + "hex": "#D9730D", + "category": "format" + }, + { + "name": "Format - Audio", + "hex": "#C14C8A", + "category": "format" + }, + { + "name": "Popover Shadow", + "hex": "0 4px 12px rgba(0,0,0,0.45)", + "category": "shadow" + } + ], + "sources": [ + "https://linear.app/now/how-we-redesigned-the-linear-ui", + "https://rogerwong.me/2024/11/how-we-redesigned-the-linear-ui-part-ii", + "https://styles.refero.design/style/90ce5883-bb24-4466-93f7-801cd617b0d1", + "https://vercel.com/geist/colors", + "https://github.com/ephraimduncan/geist-colors", + "https://developers.raycast.com/api-reference/user-interface", + "https://manual.raycast.com/themes", + "https://blakecrosley.com/guides/design/notion-calendar", + "https://blakecrosley.com/guides/design/arc", + "https://culturedcode.com/things/features/", + "https://www.eleken.co/blog-posts/empty-state-ux", + "https://muz.li/blog/dark-mode-design-systems-a-complete-guide-to-patterns-tokens-and-hierarchy/" + ] + }, + { + "key": "file-colors", + "topic": "파일 형식/확장자별 색상 컨벤션 — 신규 형식(csv/json/xlsx/svg/영상/오디오) 색상 매핑 (다크 Fluent 기준)", + "findings": [ + { + "system": "VS Code Material Icon Theme (실제 SVG fill hex)", + "takeaway": "가장 실용적인 1차 레퍼런스. 실제 렌더 색상: json=#F9A825(앰버/골드), svg=#FFB300(앰버), table(csv/xlsx/tsv 공용)=#8BC34A(라임그린), image(png/jpg/gif/webp)=#26A69A(틸), video(mp4 등)=#FF9800(오렌지), audio(mp3 등)=#EF5350(레드), markdown=#42A5F5(블루), html=#E65100(딥오렌지), xml=#8BC34A(그린), pdf=#EF5350, yaml=#FF5252. 핵심: json/svg는 '노랑~앰버', 데이터테이블(csv/xlsx)은 '그린'이 표준. 이 앱은 png를 보라로 쓰지만 Material은 이미지 전체를 틸로 묶음 → 신규형식만 추가하는 입장에선 Material의 카테고리 색만 차용 권장.", + "url": "https://github.com/material-extensions/vscode-material-icon-theme" + }, + { + "system": "Seti UI 아이콘 테마 (Solarized 9색 팔레트)", + "takeaway": "확장자→색 매핑이 명시적. json=yellow(#B58900), csv=green(#859900), yaml=purple(#6C71C4), svg=purple(#6C71C4), 이미지(png/jpg)=purple, mp4(video)=pink(#D33682), mp3(audio)=purple, xlsx=green, pdf=red(#DC322F), markdown=blue(#268BD2), html/xml=orange(#CB4B16). 핵심 시사점: 영상은 '핑크', 오디오/벡터는 '보라' 계열을 쓴다는 또 다른 강한 컨벤션. Material과 종합하면 영상=레드~핑크~오렌지(난색), 오디오=레드~보라(난색~퍼플)로 수렴.", + "url": "https://github.com/jesseweed/seti-ui/blob/master/styles/components/icons/mapping.less" + }, + { + "system": "GitHub Linguist languages.yml (언어 식별 color)", + "takeaway": "브랜드 정체성 색이라 UI 가독성엔 부적합한 경우 있음(json=#292929 거의 검정, yaml=#CB171E 레드). 단 일부는 유용: svg=#FF9900(오렌지), css=#663399(퍼플), html=#E34C26(주황빨강), scss=#C6538C(핑크), markdown=#083FA1(딥블루). 결론: csv/json/xlsx 같은 데이터 형식엔 Linguist 대신 아이콘테마 색을 쓰는 게 맞다. svg가 오렌지/앰버라는 점만 교차검증 자료로 채택.", + "url": "https://raw.githubusercontent.com/github/linguist/master/lib/linguist/languages.yml" + }, + { + "system": "기존 FormatShiftTheme.xaml 14색 형식 팔레트", + "takeaway": "이미 Chakra/Tailwind 톤(채도 중간, 다크 배경 대비 양호)으로 통일돼 있음: pdf=#E53E3E, docx/jpg=블루(#2B6CB0/#3182CE), html=#DD6B20(오렌지), png/gif=퍼플(#805AD5/#9F7AEA), webp=#48BB78(그린), raw/tiff=틸. 신규색은 반드시 이 채도/명도대(Tailwind 500 전후, S~70% L~55%)에 맞춰야 14개와 한 세트로 보인다. 생짜 Material/Solarized hex를 그대로 넣으면 채도가 튄다 → 컨벤션의 '색상(hue)'만 가져오고 채도는 기존 톤에 맞춰 재조정 권장.", + "url": "https://github.com/material-extensions/vscode-material-icon-theme/blob/main/src/core/icons/fileIcons.ts" + } + ], + "recommendation": "신규 6개 형식의 회색을 카테고리 컨벤션 색으로 교체하되, 기존 14색과 동일한 톤(Tailwind 500대, 다크 #090A0C~#1C1E22 배경 위 대비 4.5:1+)으로 채도를 맞춘다. 3개 레퍼런스(Material 실제 hex / Seti Solarized / Linguist) 종합 결론은 다음과 같다.\\n\\n1) JSON = 앰버/골드 — Material json=#F9A825, Seti=yellow가 압도적 컨벤션. 기존 heic(#D69E2E 앰버)와 충돌하니 살짝 더 노란 #E3B341(또는 #ECC94B) 권장. '데이터=노랑' 직관.\\n2) CSV / XLSX(스프레드시트) = 그린 — Material table=#8BC34A, Seti=green 일치. 둘은 같은 데이터테이블 카테고리이므로 동일 hue로 묶되 명도만 살짝 다르게: csv=#68A063, xlsx=#38A169(엑셀 브랜드 그린과도 정합). 기존 webp(#48BB78)와 구분되게 약간 더 딥하게.\\n3) SVG(벡터) = 앰버~오렌지 — Material svg=#FFB300, Linguist svg=#FF9900 교차검증. 단 json이 노랑을 쓰므로 SVG는 핑크/마젠타로 차별화하는 것도 강한 옵션(Seti는 svg=purple, css계열은 핑크). 권장: 벡터 = 핑크-마젠타 #E05A9C 또는 앰버 #F6AD55 중 택1. json과 한 화면에 같이 뜨면 핑크 추천(노랑과 즉시 구분).\\n4) 영상(mp4 등) = 레드~오렌지(난색) — Material video=#FF9800, Seti video=pink, 통념 '영상=빨강'. 기존 pdf가 이미 레드(#E53E3E)라 충돌 → 영상은 코랄/오렌지레드 #FF7043(또는 #ED8936) 권장. 난색 유지하되 pdf와 구분.\\n5) 오디오(mp3 등) = 퍼플~핑크 — Seti audio=purple, 통념 '오디오=보라/핑크'. 기존 png/gif가 퍼플(#805AD5/#9F7AEA)이라 충돌 → 오디오는 더 핑크 쪽 바이올렛 #B794F4 또는 마젠타핑크 #ED64A6(기존 avif와 동일하니 회피) → #B794F4(라이트 퍼플) 권장.\\n6) 마크업/기타 데이터(yaml/xml 등 추가 시) = Material yaml=#FF5252, xml=#8BC34A이나 일관성 위해: yaml=오렌지 #DD6B20(기존 html과 같은 마크업군), xml=그린계 회피하고 #ECC94B(데이터)로.\\n\\n충돌 회피 최종 매핑(기존 14색과 안 겹치게): json=#E3B341(골드), csv=#68A063(라임그린), xlsx=#38A169(엑셀그린), svg=#E05A9C(핑크-마젠타, 노랑json과 즉시 구분), video=#FF7043(코랄, pdf레드와 구분), audio=#B794F4(라이트퍼플). 6색 모두 Tailwind 400~500 명도라 기존 팔레트와 한 세트로 보이고, 다크 배경 대비도 충분. 적용은 FormatShiftTheme.xaml 28~40행의 FsFmt* 패턴에 FsFmtCsv/FsFmtJson/FsFmtXlsx/FsFmtSvg/FsFmtVideo/FsFmtAudio 키를 추가하면 됨.\",\"palette\":[{\"name\":\"JSON (데이터)\",\"hex\":\"#E3B341\",\"category\":\"data\"},{\"name\":\"CSV (데이터/표)\",\"hex\":\"#68A063\",\"category\":\"data\"},{\"name\":\"XLSX (스프레드시트)\",\"hex\":\"#38A169\",\"category\":\"data\"},{\"name\":\"SVG (벡터)\",\"hex\":\"#E05A9C\",\"category\":\"vector\"},{\"name\":\"영상 mp4/mkv/webm 등 (video)\",\"hex\":\"#FF7043\",\"category\":\"video\"},{\"name\":\"오디오 mp3/wav/flac 등 (audio)\",\"hex\":\"#B794F4\",\"category\":\"audio\"},{\"name\":\"[참고] SVG 앰버 대안\",\"hex\":\"#F6AD55\",\"category\":\"vector-alt\"},{\"name\":\"[참고] 영상 오렌지 대안\",\"hex\":\"#ED8936\",\"category\":\"video-alt\"},{\"name\":\"[레퍼런스] Material json 실제 fill\",\"hex\":\"#F9A825\",\"category\":\"reference\"},{\"name\":\"[레퍼런스] Material svg 실제 fill\",\"hex\":\"#FFB300\",\"category\":\"reference\"},{\"name\":\"[레퍼런스] Material table(csv/xlsx) fill\",\"hex\":\"#8BC34A\",\"category\":\"reference\"},{\"name\":\"[레퍼런스] Material image(png/jpg) fill\",\"hex\":\"#26A69A\",\"category\":\"reference\"},{\"name\":\"[레퍼런스] Material video fill\",\"hex\":\"#FF9800\",\"category\":\"reference\"},{\"name\":\"[레퍼런스] Material audio fill\",\"hex\":\"#EF5350\",\"category\":\"reference\"},{\"name\":\"[레퍼런스] Seti video=pink\",\"hex\":\"#D33682\",\"category\":\"reference\"},{\"name\":\"[레퍼런스] Seti audio/svg=purple\",\"hex\":\"#6C71C4\",\"category\":\"reference\"}]", + "sources": [ + "https://github.com/material-extensions/vscode-material-icon-theme", + "https://github.com/material-extensions/vscode-material-icon-theme/blob/main/src/core/icons/fileIcons.ts", + "https://raw.githubusercontent.com/material-extensions/vscode-material-icon-theme/main/icons/json.svg", + "https://raw.githubusercontent.com/material-extensions/vscode-material-icon-theme/main/icons/svg.svg", + "https://raw.githubusercontent.com/material-extensions/vscode-material-icon-theme/main/icons/table.svg", + "https://raw.githubusercontent.com/material-extensions/vscode-material-icon-theme/main/icons/image.svg", + "https://raw.githubusercontent.com/material-extensions/vscode-material-icon-theme/main/icons/video.svg", + "https://raw.githubusercontent.com/material-extensions/vscode-material-icon-theme/main/icons/audio.svg", + "https://github.com/jesseweed/seti-ui/blob/master/styles/components/icons/mapping.less", + "https://github.com/microsoft/vscode/blob/main/extensions/theme-seti/icons/vs-seti-icon-theme.json", + "https://raw.githubusercontent.com/github/linguist/master/lib/linguist/languages.yml", + "https://github.com/nvim-tree/nvim-web-devicons/blob/master/lua/nvim-web-devicons/icons-default.lua" + ] + }, + { + "key": "components", + "topic": "FormatShift Utility 다크 Fluent 데스크톱 앱 UI 컴포넌트 리서치 (API키 입력 / 상태배지 / 미설치 안내 / 세그먼트·토글·셀렉트)", + "findings": [ + { + "system": "Cursor (Settings → Models → API Keys)", + "takeaway": "API 키 입력은 단일 행에 [마스킹된 입력 필드] + [Verify 버튼]을 나란히 둔다. 저장 즉시가 아니라 명시적 'Verify'로 키 유효성을 검증하고, 성공 시 녹색 체크/‘valid’, 실패 시 인라인 에러를 표시. Cursor의 핵심 교훈은 '검증 피드백이 없으면(no verify/no error) 사용자가 키가 적용됐는지 알 수 없어 불신한다'는 점 — 반드시 가시적 검증 결과(성공/실패 배지)를 줘야 한다. OpenAI/Anthropic 키를 프로바이더별 섹션으로 분리.", + "url": "https://cursor.com/help/models-and-usage/api-keys" + }, + { + "system": "Zed (Agent: open settings)", + "takeaway": "키는 settings 파일(평문)에 저장하지 않고 OS 키체인(Windows에선 Credential Manager/DPAPI)에 저장한다고 명시. 또한 OPENAI_API_KEY / ANTHROPIC_API_KEY 환경변수가 있으면 자동 사용하고 입력 필드에 '환경변수에서 감지됨' 상태를 노출. FormatShift도 DPAPI로 키를 암호화 저장하고, 입력란 옆에 '환경변수 감지됨' 또는 '키체인에 저장됨' 보조 텍스트를 #8B909A 색으로 표기하면 신뢰도가 올라간다.", + "url": "https://zed.dev/docs/ai/llm-providers" + }, + { + "system": "Raycast (Extension Preferences / Form.PasswordField)", + "takeaway": "민감 입력 전용 'password' preference 타입과 Form.PasswordField를 별도 제공 — 일반 textfield와 구분된 보안 필드. 빈 상태에는 placeholder로 형식 힌트(예: 'sk-...')를 주는 것을 권장. FormatShift도 PasswordBox 기반 마스킹 + 우측에 eye(👁) 토글로 평문 확인 + 'sk-ant-...'/'sk-...' 형식 placeholder를 적용.", + "url": "https://developers.raycast.com/api-reference/preferences" + }, + { + "system": "Radix Colors (Dark scales — 다크 UI 시맨틱 색 사실상 표준)", + "takeaway": "shadcn/Radix Badge가 쓰는 12-step 다크 스케일. 배지/dot의 solid는 step9, 배지 텍스트는 step11, 배지 배경(subtle fill)은 step3을 쓰는 것이 규칙(같은 step 번호 = 같은 UI 역할). 신규 회색 형식 색을 이 값으로 채우면 일관된 시맨틱이 됨 — Success/Green9 #30a46c·11 #3dd68c·3 #132d21, Warning/Amber9 #ffc53d·11 #ffca16·3 #302008, Danger/Red9 #e5484d·11 #ff9592·3 #3b1219, Info/Blue9 #0090ff·11 #70b8ff·3 #0d2847. '손실(lossy)' 배지는 Amber(주의), '비손실'은 Green을 권장.", + "url": "https://www.radix-ui.com/themes/docs/components/badge" + }, + { + "system": "Linear (status dot/pill)", + "takeaway": "상태는 채워진 점(dot)/얇은 pill로 표현하고, 다크모드에서 LCH 색공간으로 명도를 인지적으로 균일하게 맞춰 어느 색이든 같은 step이면 같은 밝기로 보이게 함(빨강·노랑이 동일 명도로 보임). 교훈: 14개 형식색을 그냥 채도만 다르게 두지 말고 '명도(밝기)를 통일'해야 다크 배경(#090A0C~#1C1E22)에서 한 형식만 튀지 않는다. 상태 dot은 8px 원 + 6px gap + 라벨 패턴이 표준.", + "url": "https://linear.app/now/how-we-redesigned-the-linear-ui" + }, + { + "system": "Microsoft Fluent / WinUI InfoBar (미설치·경고 안내)", + "takeaway": "외부 의존성(FFmpeg/LibreOffice 등) 미설치 안내는 Severity(Informational/Success/Warning/Error)로 색·아이콘·접근성을 일괄 지정하는 InfoBar 패턴이 정석. 다크에서는 채도 높은 원색 대신 surface 위 subtle tint(Radix step3 배경 + step11 텍스트)를 쓰고, 우측에 단일 다운로드 CTA 버튼 하나만 배치('하나의 명확한 경로'). FormatShift는 Wpf.Ui FluentWindow이므로 ui:InfoBar Severity='Warning'을 그대로 활용 가능.", + "url": "https://learn.microsoft.com/en-us/windows/apps/design/controls/infobar" + }, + { + "system": "빈 상태 / 다운로드 CTA (Atlassian·Material·PatternFly)", + "takeaway": "의존성 미설치 = 빈/경고 상태. 효과적 빈 상태는 항상 단 하나의 명확한 CTA를 제공('vague한 3개보다 명확한 1개'). 구성: 아이콘/일러스트 + 무엇이 없고 왜 필요한지 1줄 + 'XXX 다운로드' primary 버튼 + 보조로 '이미 설치했다면 경로 지정' 링크. 자동 감지 후에는 상태를 Success 배지로 전환해 진행 가능함을 즉시 알림.", + "url": "https://atlassian.design/components/empty-state/" + }, + { + "system": "Apple HIG / Mobbin (Segmented Control vs Toggle vs Select)", + "takeaway": "선택 기준이 명확함: 2개 이진 옵션 → Switch(토글), 상호배타 2~5개 + 즉시 전환 → Segmented Control(모두 보임/계층 없음), 5개 초과 또는 동적 → Select(드롭다운). 6개 이상을 세그먼트에 넣으면 너무 넓고 빽빽해 가독성 붕괴. 다크에서는 active 세그먼트를 '채워진(filled)' 처리로 확실히 구분해야 함(accent #3B72FF fill 또는 elevated surface).", + "url": "https://developer.apple.com/design/human-interface-guidelines/segmented-controls" + } + ], + "recommendation": "FormatShift(다크 Fluent, accent #3B72FF) 4개 컴포넌트 적용안.\n\n(1) API 키 설정 화면: 좌측 옵션패널이 아닌 별도 Settings 화면에 OpenAI/Anthropic 섹션을 분리. 각 섹션은 한 행에 [PasswordBox(마스킹, placeholder 'sk-ant-...')] + [👁 평문토글] + [Verify 버튼] 배치(Cursor 패턴). 저장은 Windows DPAPI/Credential Manager로 암호화(Zed 패턴), 평문 settings 금지. 환경변수(ANTHROPIC_API_KEY) 감지 시 입력란 위에 '환경변수에서 감지됨' 보조텍스트 #8B909A. Verify 결과를 우측에 상태 배지로: 성공 = Green dot #30a46c + 'Valid', 실패 = Red #e5484d + 인라인 에러, 검증중 = spinner. 검증 전 빈 필드는 submit 전까지 에러 표시 안 함(premature validation 회피).\n\n(2) 상태/손실 배지: 14개 형식색 + 신규 6종(csv/json/xlsx/svg/영상/오디오)을 Radix 다크 step9 기준으로 명도 통일(Linear 교훈). 손실 여부 배지는 시맨틱 고정 — 비손실 = Green(#30a46c solid / #132d21 bg / #3dd68c text), 손실 = Amber(#ffc53d / #302008 / #ffca16), 변환불가 = Red(#e5484d / #3b1219 / #ff9592). pill 형태: 높이 20~22px, radius 6px(또는 full), 수평 패딩 8px, 상태 dot은 8px 원 + 6px gap.\n\n(3) 의존성 미설치 안내: Wpf.Ui InfoBar Severity='Warning'(Amber tint #302008 배경 + #ffca16 텍스트/아이콘)로 'FFmpeg가 필요합니다 — 영상/오디오 변환에 필요' 1줄 + 단일 'FFmpeg 다운로드' CTA(accent #3B72FF) + '이미 설치됨? 경로 지정' 보조 링크. 자동 감지 성공 시 Green Success InfoBar로 전환. 큐의 해당 형식 칩은 미설치 동안 회색 유지 → 설치 후 정상색 점등.\n\n(4) 세그먼트/토글/셀렉트: 출력 형식처럼 2~5개 즉시 전환은 Segmented Control(active 세그먼트 accent #3B72FF fill 또는 elevated #1C1E22 surface로 명확히 구분). on/off(예: '메타데이터 보존')는 ToggleSwitch. 5개 초과(형식 전체 목록 등)는 ComboBox(Select). 6개 이상을 세그먼트에 넣지 말 것.\n\n간격/반경 통일: 컴포넌트 radius 6px, 컨테이너 8px, 컨트롤 높이 32px, 배지 높이 20~22px, dot 8px, gap 6~8px.", + "palette": [ + { + "name": "Success solid (비손실 dot/배지)", + "hex": "#30A46C", + "category": "status-success" + }, + { + "name": "Success text (배지 텍스트)", + "hex": "#3DD68C", + "category": "status-success" + }, + { + "name": "Success bg (subtle fill)", + "hex": "#132D21", + "category": "status-success" + }, + { + "name": "Warning solid (손실/주의 dot)", + "hex": "#FFC53D", + "category": "status-warning" + }, + { + "name": "Warning text (배지 텍스트)", + "hex": "#FFCA16", + "category": "status-warning" + }, + { + "name": "Warning bg (InfoBar tint)", + "hex": "#302008", + "category": "status-warning" + }, + { + "name": "Danger solid (변환불가/에러 dot)", + "hex": "#E5484D", + "category": "status-danger" + }, + { + "name": "Danger text (배지 텍스트)", + "hex": "#FF9592", + "category": "status-danger" + }, + { + "name": "Danger bg (subtle fill)", + "hex": "#3B1219", + "category": "status-danger" + }, + { + "name": "Info solid (정보 dot)", + "hex": "#0090FF", + "category": "status-info" + }, + { + "name": "Info text (배지 텍스트)", + "hex": "#70B8FF", + "category": "status-info" + }, + { + "name": "Info bg (subtle fill)", + "hex": "#0D2847", + "category": "status-info" + }, + { + "name": "Neutral text (보조/감지됨 텍스트)", + "hex": "#8B909A", + "category": "neutral-existing" + }, + { + "name": "Accent (앱 기존 — CTA/active 세그먼트)", + "hex": "#3B72FF", + "category": "accent-existing" + } + ], + "sources": [ + "https://cursor.com/help/models-and-usage/api-keys", + "https://forum.cursor.com/t/no-verify-button-when-setting-custom-openai-api-key-in-settings-models/140843", + "https://zed.dev/docs/ai/llm-providers", + "https://developers.raycast.com/api-reference/preferences", + "https://developers.raycast.com/api-reference/user-interface", + "https://uxpatterns.dev/patterns/forms/password", + "https://www.radix-ui.com/themes/docs/components/badge", + "https://www.radix-ui.com/colors", + "https://github.com/radix-ui/colors/blob/main/src/dark.ts", + "https://linear.app/now/how-we-redesigned-the-linear-ui", + "https://linear.app/changelog/2020-12-04-themes", + "https://learn.microsoft.com/en-us/windows/apps/design/controls/infobar", + "https://github.com/microsoft/microsoft-ui-xaml/issues/3744", + "https://atlassian.design/components/empty-state/", + "https://www.patternfly.org/components/empty-state/", + "https://www.setproduct.com/blog/empty-state-ui-design", + "https://developer.apple.com/design/human-interface-guidelines/segmented-controls", + "https://mobbin.com/glossary/segmented-control", + "https://wpfui.lepo.co/documentation/themes.html" + ] + } +] \ No newline at end of file diff --git a/docs/ssot/_data/design_synth.json b/docs/ssot/_data/design_synth.json new file mode 100644 index 0000000..8a84266 --- /dev/null +++ b/docs/ssot/_data/design_synth.json @@ -0,0 +1,145 @@ +{ + "vision": "FormatShift를 \"변환 전에 결과를 신뢰할 수 있는\" 다크 유틸리티로 끌어올린다. 세 축이 직교한다: (1) 형식색(FsFmt*) = '무엇을/무엇으로', (2) 손실 등급색(FsLoss*) = '얼마나 보존되나', (3) 상태색(FsStatus*, Radix dark) = 'AI키·외부도구가 준비됐나'. 사용자가 명시 요구한 '형식 색상'과 'AI 설정창'을 최우선으로 하되, 형식색은 이미 직전 커밋(25bebb4)에서 구현 완료된 상태이므로 잔여 마감(빈 상태 칩 텍스트)만 처리하고, 남은 에너지를 AI 설정창과 손실 등급 배지에 집중한다. 모든 신규 토큰은 기존 FsBg/FsText/FsFmt 명명 규칙으로만 확장하고, Core(LossClass/IChatClient/ExternalToolDetector)는 SSOT 그대로 재사용해 UI 계층만 손댄다.", + "priorities": [ + { + "title": "AI API 키 설정 창(SettingsWindow) 신설 — 사용자 명시 요구, 현재 키 입력 UI가 전무함", + "impact": "high", + "effort": "high" + }, + { + "title": "App에 Settings(ISettingsStore) 공유 노출 + ExternalToolDetector public 승격 — 설정창의 전제 인프라", + "impact": "high", + "effort": "low" + }, + { + "title": "손실 등급(Lossless/Container/Recode/Rasterize) 배지·dot 시스템 — Core LossClass가 이미 있는데 UI 미노출, 체감 가치 큼", + "impact": "high", + "effort": "medium" + }, + { + "title": "Radix dark 상태 시맨틱 토큰 + 손실 토큰 + FsPasswordInputStyle 추가 (테마 1회 확장)", + "impact": "high", + "effort": "low" + }, + { + "title": "capability 경고를 평문 TextBlock → InfoBar(Amber tint) 패턴으로 격상", + "impact": "medium", + "effort": "low" + }, + { + "title": "형식색 잔여 마감: 빈 큐 상태 지원형식 칩 텍스트 갱신(신규 16형식 반영) — 색 토큰/매핑은 이미 완료됨", + "impact": "medium", + "effort": "low" + }, + { + "title": "전반 UX 폴리시(인터랙티브 빈 상태·hover 페이드인·accent bar·tabular-nums)", + "impact": "medium", + "effort": "medium" + } + ], + "implementationPlan": [ + { + "step": "테마 토큰 1회 확장 — Radix dark 상태색 + 손실 등급색 + PasswordBox 스타일", + "files": [ + "src/Everything2Everything.App/Views/FormatShiftTheme.xaml" + ], + "detail": "FsAccent 블록(25행) 아래에 추가: (A) Radix dark 상태 12토큰 — FsStatusSuccess Solid #30A46C/Text #3DD68C/Bg #132D21, Warn #FFC53D/#FFCA16/#302008, Danger #E5484D/#FF9592/#3B1219, Info #0090FF/#70B8FF/#0D2847. (B) 손실 등급 토큰 — FsLossLossless=Success계열(녹색), FsLossContainer=중립회색 dot(FsTextSecondary #8B909A 재사용, userDecision 참조), FsLossRecode=Amber계열, FsLossRasterize=Danger계열의 dot/text/bg 3쌍. (C) Geist 투명보더 FsBorderHairline=#14FFFFFF(8% 알파). 그리고 FsPathInputStyle(287행) 복제해 FsPasswordInputStyle(TargetType=PasswordBox, PART_ContentHost 대신 PasswordBox 템플릿, FsBgInput 배경/FsFontMono/radius 6, 포커스 시 BorderBrush=FsAccentBlue) 추가. 형식색 6토큰(43-57행)은 이미 존재하므로 건드리지 않음." + }, + { + "step": "App에 ISettingsStore 공유 노출 + Bootstrap 시그니처 확장", + "files": [ + "src/Everything2Everything.Core/Everything2EverythingBootstrap.cs", + "src/Everything2Everything.App/App.xaml.cs" + ], + "detail": "Bootstrap.CreateDefault()가 내부에서 new DpapiSettingsStore()를 만들어 LlmProvider에만 넘기는데(9·26행), 동일 인스턴스를 App이 공유해야 함. CreateDefault(out ISettingsStore settings) 오버로드(또는 (ConversionEngine, ISettingsStore) 튜플 반환)를 추가. App.xaml.cs(10행)에서 Engine과 함께 'public ISettingsStore Settings { get; }'를 같은 store로 초기화해 LlmProvider·SettingsWindow가 키 'openai.apikey'/'anthropic.apikey'/'ai.backend'/'ai.model'을 공유하게 함." + }, + { + "step": "ExternalToolDetector를 internal→public 승격(또는 public ToolStatusProbe 래퍼)", + "files": [ + "src/Everything2Everything.Core/Converters/ExternalToolDetector.cs" + ], + "detail": "현재 internal static(5행). 설정창 외부도구 섹션이 App 계층에서 TryFindFfmpeg/TryFindLibreOfficeSoffice/IsWordComAvailable를 호출해야 하므로 class를 public으로 바꾸거나, 이미 public인 표면을 원하면 Core에 얇은 'public static class ToolStatusProbe' 래퍼를 신설해 3개 메서드를 위임. 시그니처(out string)는 그대로 유지." + }, + { + "step": "SettingsWindow.xaml 신설 — 3카드(AI백엔드/모델/외부도구) 모달", + "files": [ + "src/Everything2Everything.App/Views/SettingsWindow.xaml" + ], + "detail": "FluentWindow(또는 기존 Window) 640x720, FormatShiftTheme.xaml 머지 + FsBgBase 배경. 32px TitleBar + ScrollViewer 3카드(FsBgSurface #1C1E22, 보더 FsBorderHairline 8%, radius 6). 카드1: '활성 백엔드' FsSegmentStyle 3분할(Auto/OpenAI/Anthropic) + OpenAI·Anthropic 각 행에 [PasswordBox(FsPasswordInputStyle, placeholder sk-.../sk-ant-...)] + [👁 평문토글] + [Verify 버튼] + 우측 status dot(8px)+배지 자리. 빈 필드엔 에러 미표시(premature validation 회피). 카드2: 백엔드별 모델 ComboBox + '비워두면 기본 모델' 캡션. 카드3: FFmpeg/LibreOffice/MS Word COM StatusRow 3개(dot + 도구명 + 상태라벨 + 미설치 시 Warn tint 배경 + 다운로드 CTA). 푸터: 저장·닫기. DiagnoseWindow.xaml을 구조 레퍼런스로 사용." + }, + { + "step": "SettingsWindow.xaml.cs 신설 — 로드/Verify/저장/외부도구 감지 핸들러", + "files": [ + "src/Everything2Everything.App/Views/SettingsWindow.xaml.cs" + ], + "detail": "생성자 new SettingsWindow(ISettingsStore settings, ConversionEngine engine). 로드: Settings.Get로 키 존재 시 '저장된 키 있음' placeholder, 환경변수(OPENAI_API_KEY/ANTHROPIC_API_KEY) 감지 시 EnvHint 노출. OnBackendSegmentClick(3-way 토글), OnToggleReveal(PasswordBox↔평문 TextBox 스왑), OnVerifyClick(new OpenAiChatClient/AnthropicChatClient(key).CompleteAsync('ping','ping',model,maxTokens=1,ct) → 200=Success배지/401·403=Danger/네트워크오류=Warn, 검증중 indeterminate ProgressBar). OnSaveClick(Settings.Set). OnDownloadClick(Process.Start로 gyan.dev/ffmpeg·libreoffice.org 브라우저 오픈). 외부도구 섹션은 ToolStatusProbe.TryFind*로 채워 dot/배지 매핑." + }, + { + "step": "MainWindow에 ⚙ 설정 진입점 추가", + "files": [ + "src/Everything2Everything.App/Views/MainWindow.xaml", + "src/Everything2Everything.App/Views/MainWindow.xaml.cs" + ], + "detail": "MainWindow.xaml Actions StackPanel(336행) 'Register Menu' 앞에 ⚙ Button(FsSecondaryButtonStyle, 32x32, gear Path, Click=OnSettingsClick). MainWindow.xaml.cs에 OnSettingsClick: var app=(App)Application.Current; new SettingsWindow(app.Settings, app.Engine){Owner=this}.ShowDialog(); 닫힌 뒤 _ = RefreshCapabilityStatusAsync()로 키 반영(모달이라 동기화 단순)." + }, + { + "step": "손실 등급 팔레트 헬퍼 + dot/pill 스타일", + "files": [ + "src/Everything2Everything.App/Views/MainWindow.xaml.cs", + "src/Everything2Everything.App/Views/FormatShiftTheme.xaml" + ], + "detail": "MainWindow.xaml.cs FormatPalette(1146행) 옆에 'internal static class LossPalette'를 동형으로 추가: LossClass→(DotBrushKey, TextBrushKey, BgBrushKey, ShortLabel, KoLabel). 가시 라벨은 한국어(무손실/재포장/재인코딩/래스터화), 영어 enum명은 툴팁(ConversionPair.cs XML doc 문구 재사용). FormatShiftTheme.xaml에 FsLossDotStyle(Ellipse 8x8)+FsLossPillStyle(Border radius6, Padding 8,2, Height22) 추가." + }, + { + "step": "손실 등급 계산 + 출력 드롭다운/출력 배지 노출", + "files": [ + "src/Everything2Everything.App/Views/MainWindow.xaml.cs", + "src/Everything2Everything.App/Views/MainWindow.xaml" + ], + "detail": "MainWindow.xaml.cs에 LossGradeFor(IEnumerable sources, string outputExt): 각 입력에 engine.Providers.Graph.FindBestPath(in,out)→path.Max(e=>e.Loss)를 구하고 큐 전체의 worst-case(Max severity)를 등급으로 채택. RefreshAvailableOutputFormats(852행)의 ComboBoxItem.Content를 dot+라벨 템플릿으로 교체하거나 ItemTemplate 도입해 각 형식 옆 등급 dot 예고. OutputFormatBadge(xaml 89행) 아래에 현재 선택 등급 pill 1줄(LossGradeBadge) 추가. 선택이 Rasterize면 OutputFormatHint(xaml 102행)에 '벡터/텍스트 편집성이 사라집니다(단방향)'를 FsLossRasterizeText 색으로 표시." + }, + { + "step": "큐 row 손실 dot + capability 경고 InfoBar화 + 빈 상태 칩 텍스트 마감", + "files": [ + "src/Everything2Everything.App/Views/MainWindow.xaml.cs", + "src/Everything2Everything.App/Views/MainWindow.xaml" + ], + "detail": "QueueItem에 LossClass + 파생속성(LossDotBrush/LossPillVisibility/LossLabel/LossBrush) 추가. 큐 row DataTemplate(386행)의 형식배지 우상단 또는 Grid.Column 3 근처에 dot 배치, 절제: Lossless/Container는 dot만, Recode/Rasterize만 pill(LossPillVisibility 조건부). CapabilityStatusText(xaml 233행 평문 TextBlock)를 Background=FsStatusWarnBg(#302008) + 경고아이콘 + FsStatusWarnText(#FFCA16) + 'Diagnose' CTA의 Border(InfoBar 패턴)로 교체. 형식색은 이미 완료됐으므로 DropZoneEmpty(xaml 377행)의 지원형식 칩 텍스트만 신규 16형식 포함하도록 갱신(현재 'PNG·JPG·GIF·HEIC·RAW·PDF·DOCX·HTML·HWP/HWPX'에 CSV/JSON/XLSX/SVG/MP4/MP3 등 추가, 또는 WrapPanel 형식색 칩으로 교체)." + }, + { + "step": "빌드/검증 — publish + 카스케이드 재등록", + "files": [ + "src/Everything2Everything.App/Everything2Everything.App.csproj" + ], + "detail": "dotnet build로 경고0/오류0 확인 후, MEMORY project_build_pipeline 시퀀스대로 publish + 카스케이드 재등록. 사용자가 직접 push & GUI 검증(feedback_user_workflow: 단일 commit 선호). Verify 1토큰 핑은 소액 과금 가능성을 커밋 메시지/PR에 명시." + } + ], + "userDecisions": [ + { + "question": "Verify(키 검증) 방식을 실제 API 1토큰 핑으로 할지, 형식 정규식 검사로 할지?", + "options": [ + "실제 API 1토큰 핑(CompleteAsync maxTokens=1) — 무효 키까지 확실히 잡지만 소액 과금 가능", + "sk-/sk-ant- prefix 정규식 검사 — 무과금이지만 무효 키는 못 잡음", + "2단계: prefix 검사로 Verify 버튼 활성화 → 클릭 시에만 실제 핑" + ], + "recommendation": "3번(2단계). 빈/형식오류 키는 무과금으로 즉시 걸러 Verify 비활성, 사용자가 명시적으로 Verify를 누를 때만 실제 핑을 보내 과금 동의를 분명히 함. Cursor가 권장하는 실제 검증의 신뢰성과 과금 투명성을 모두 충족." + }, + { + "question": "손실 등급 'Container'를 Blue(Radix Blue9)로 둘지, 무채색 회색 dot으로 둘지?", + "options": [ + "Blue #0090FF — Radix 시맨틱 일관", + "중립 회색 dot(FsTextSecondary #8B909A) — 구조변경은 손실이 아니므로 조용히 처리", + "Teal 등 제3의 색" + ], + "recommendation": "2번(중립 회색). 앱 액센트 FsAccentBlue(#3B72FF)와 hue가 가까워 Blue는 '클릭 가능'으로 오인될 수 있음. Container는 손실이 아니므로 색을 아껴 조용히 두고, Blue는 순수 info용으로 보존하는 게 4등급 그라데이션(녹→회→앰버→레드) 가독성에 유리." + }, + { + "question": "외부도구 '경로 지정'을 ISettingsStore 영구 키(ffmpeg.dir)로 저장할지, 기존 %LOCALAPPDATA% 폴더 복사 안내만 할지?", + "options": [ + "ISettingsStore 'ffmpeg.dir' 신설 — 사용자가 임의 경로 지정 가능, ExternalToolDetector도 이 키 우선 조회하도록 확장 필요", + "기존 %LOCALAPPDATA%\\Everything2Everything\\ffmpeg 폴더 복사 안내만 — 코드 변경 최소(이미 ExternalToolDetector가 이 폴더를 1순위 조회)" + ], + "recommendation": "2번(폴더 안내). ExternalToolDetector.TryFindFfmpeg이 이미 %LOCALAPPDATA%\\Everything2Everything\\ffmpeg를 1순위로 보고 있어(54-63행) 추가 키·조회 분기 없이 다운로드 CTA + '이 폴더에 넣으세요' 안내만으로 충분. 설정창 1차 릴리스 범위를 줄여 AI 키 검증에 집중하고, 임의 경로 지정은 후속으로." + } + ] +} \ No newline at end of file diff --git a/src/Everything2Everything.App/Views/FormatShiftTheme.xaml b/src/Everything2Everything.App/Views/FormatShiftTheme.xaml index 795d269..784011c 100644 --- a/src/Everything2Everything.App/Views/FormatShiftTheme.xaml +++ b/src/Everything2Everything.App/Views/FormatShiftTheme.xaml @@ -40,6 +40,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Inter, Segoe UI Variable Text, Segoe UI JetBrains Mono, Cascadia Mono, Consolas diff --git a/src/Everything2Everything.App/Views/MainWindow.xaml.cs b/src/Everything2Everything.App/Views/MainWindow.xaml.cs index 7510b16..b3db981 100644 --- a/src/Everything2Everything.App/Views/MainWindow.xaml.cs +++ b/src/Everything2Everything.App/Views/MainWindow.xaml.cs @@ -824,22 +824,22 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow new(".html", "HTML", "HTML", "FsFmtHtml"), new(".md", "Markdown", "MD", "FsFmtOther"), new(".txt", "텍스트", "TXT", "FsFmtOther"), - new(".csv", "CSV", "CSV", "FsFmtOther"), - new(".json", "JSON", "JSON", "FsFmtOther"), - new(".xlsx", "Excel", "XLSX", "FsFmtOther"), - new(".svg", "SVG", "SVG", "FsFmtOther"), - new(".mp4", "MP4", "MP4", "FsFmtOther"), - new(".webm", "WebM", "WEBM", "FsFmtOther"), - new(".mkv", "MKV", "MKV", "FsFmtOther"), - new(".mov", "MOV", "MOV", "FsFmtOther"), - new(".avi", "AVI", "AVI", "FsFmtOther"), - new(".mp3", "MP3", "MP3", "FsFmtOther"), - new(".aac", "AAC", "AAC", "FsFmtOther"), - new(".m4a", "M4A", "M4A", "FsFmtOther"), - new(".opus", "Opus", "OPUS", "FsFmtOther"), - new(".ogg", "OGG", "OGG", "FsFmtOther"), - new(".flac", "FLAC", "FLAC", "FsFmtOther"), - new(".wav", "WAV", "WAV", "FsFmtOther"), + new(".csv", "CSV", "CSV", "FsFmtCsv"), + new(".json", "JSON", "JSON", "FsFmtJson"), + new(".xlsx", "Excel", "XLSX", "FsFmtXlsx"), + new(".svg", "SVG", "SVG", "FsFmtSvg"), + new(".mp4", "MP4", "MP4", "FsFmtVideo"), + new(".webm", "WebM", "WEBM", "FsFmtVideo"), + new(".mkv", "MKV", "MKV", "FsFmtVideo"), + new(".mov", "MOV", "MOV", "FsFmtVideo"), + new(".avi", "AVI", "AVI", "FsFmtVideo"), + new(".mp3", "MP3", "MP3", "FsFmtAudio"), + new(".aac", "AAC", "AAC", "FsFmtAudio"), + new(".m4a", "M4A", "M4A", "FsFmtAudio"), + new(".opus", "Opus", "OPUS", "FsFmtAudio"), + new(".ogg", "OGG", "OGG", "FsFmtAudio"), + new(".flac", "FLAC", "FLAC", "FsFmtAudio"), + new(".wav", "WAV", "WAV", "FsFmtAudio"), }; private bool _suppressFormatChanged; @@ -1160,6 +1160,15 @@ internal static class FormatPalette "bmp" => ("BMP", "FsFmtBmp"), "raw" or "dng" or "nef" or "cr2" or "cr3" or "arw" or "raf" or "orf" or "rw2" or "srw" or "pef" => ("RAW", "FsFmtRaw"), + // 신규 카테고리 (output AllFormats 매핑과 동일 hue 유지) + "csv" => ("CSV", "FsFmtCsv"), + "json" => ("JSON", "FsFmtJson"), + "xlsx" or "xls" => ("XLSX", "FsFmtXlsx"), + "svg" => ("SVG", "FsFmtSvg"), + "mp4" or "webm" or "mkv" or "mov" or "avi" or "m4v" + => (ext.ToUpperInvariant(), "FsFmtVideo"), + "mp3" or "aac" or "m4a" or "opus" or "ogg" or "oga" or "flac" or "wav" + => (ext.ToUpperInvariant(), "FsFmtAudio"), _ => (ext.ToUpperInvariant(), "FsFmtOther"), }; }