refactor(popup): 5종 팝업 _i18n 주입 + 색상 토큰화 (WS-POPUP)

- 4종 팝업(command/history/result/recording-tip) _i18n 주입:
  WindowManager getPopupI18nStrings() + 팝업 script applyI18nToHtml()
- 한국어/영어 하드코딩 -> popup.* i18n 키 (ko/en 17키)
- theme-vars.ts에 --d3-wave-1~9, --d3-status-error/success 토큰 추가 (6 테마)
- 팝업 style.css hex -> CSS var 토큰
- packages/i18n getI18n() (main 프로세스용) 추가
SKIP: caption-overlay _i18n(별도 IPC 구조), wave box-shadow 보간(opacity 단순화)
정책: docs/REFACTOR_POLICY.md DP3, 이식 인사이트 P5
This commit is contained in:
Yun Chan 2026-07-22 02:37:58 +09:00
parent 660e622a93
commit 00a99e4087
15 changed files with 256 additions and 35 deletions

View file

@ -10,6 +10,7 @@ import { getLogger } from '../services/LoggerService'
import { getIsQuitting } from '../lifecycle'
import { configGet } from '../services/ConfigService'
import { buildPopupThemeCss } from '@d3ro/ui/theme-vars'
import { getI18n } from '@d3ro/i18n'
const logger = getLogger('WindowManager')
@ -28,6 +29,39 @@ function injectPopupTheme(win: BrowserWindow): void {
})
}
// ── 팝업 i18n 문자열 가져오기 ─────────────────────────────
/**
* i18n .
*/
function getPopupI18nStrings(): Record<string, string> {
const locale = (configGet('language') as string) || 'ko'
const { t } = getI18n(locale as 'ko' | 'en' | 'ja' | 'zh' | 'zh-TW' | 'es' | 'fr' | 'de' | 'pt' | 'ru' | 'vi' | 'th')
return {
// command-popup
commandSelect: t('popup.commandSelect'),
hintsSelect: t('popup.hints.select'),
hintsApply: t('popup.hints.apply'),
hintsClear: t('popup.hints.clear'),
hintsClose: t('popup.hints.close'),
noCommands: t('popup.noCommands'),
noCommand: t('popup.noCommand'),
// history-popup
hintsSelectEn: t('popup.hints.selectEn'),
hintsPaste: t('popup.hints.paste'),
noHistory: t('popup.noHistory'),
timeJustNow: t('popup.time.justNow'),
timeMinutesAgo: t('popup.time.minutesAgo'),
timeHoursAgo: t('popup.time.hoursAgo'),
timeDaysAgo: t('popup.time.daysAgo'),
// result-popup
copy: t('popup.copy'),
// recording-tip
errorDefault: t('popup.error.default'),
// caption-overlay
captionLoading: t('popup.caption.loading'),
}
}
// ── 윈도우 참조 ───────────────────────────────────────
let mainWindow: BrowserWindow | null = null
@ -181,7 +215,7 @@ export function showRecordingTip(
win.setBounds({ x, y, width: TIP_WIDTH, height: TIP_HEIGHT })
// 상태 전송 후 즉시 show (2-phase 제거 — 숨겨진 윈도우의 렌더러 비활성 문제 방지)
win.webContents.send(IPC_CHANNELS.WINDOW.TIP_STATE_CHANGED, { state, ...params })
win.webContents.send(IPC_CHANNELS.WINDOW.TIP_STATE_CHANGED, { state, _i18n: getPopupI18nStrings(), ...params })
if (!win.isVisible()) {
win.showInactive()
}
@ -198,7 +232,7 @@ export function updateRecordingTipState(
params?: { text?: string; errorMessage?: string }
): void {
if (recordingTipWindow && !recordingTipWindow.isDestroyed()) {
recordingTipWindow.webContents.send(IPC_CHANNELS.WINDOW.TIP_STATE_CHANGED, { state, ...params })
recordingTipWindow.webContents.send(IPC_CHANNELS.WINDOW.TIP_STATE_CHANGED, { state, _i18n: getPopupI18nStrings(), ...params })
}
}
@ -268,7 +302,7 @@ export function showResultPopup(text: string, autoHideMs = 5000): void {
const win = getResultPopupWindow()
// Phase 1: prepare
win.webContents.send(IPC_CHANNELS.POPUP_RESULT.PREPARE, { text })
win.webContents.send(IPC_CHANNELS.POPUP_RESULT.PREPARE, { text, _i18n: getPopupI18nStrings() })
const handler = (_event: Electron.IpcMainEvent, data: { width: number; height: number }) => {
ipcMain.removeListener(IPC_CHANNELS.POPUP_RESULT.MEASURED, handler)
@ -369,7 +403,7 @@ export function showHistoryPopup(entries: Array<Record<string, unknown>>): void
win.setBounds({ x, y, width: popupWidth, height: popupHeight })
win.webContents.send(IPC_CHANNELS.POPUP_HISTORY.SHOW_ITEMS, { entries })
win.webContents.send(IPC_CHANNELS.POPUP_HISTORY.SHOW_ITEMS, { entries, _i18n: getPopupI18nStrings() })
if (!win.isVisible()) {
win.showInactive()
@ -456,7 +490,7 @@ export function showCommandPopup(commands: Array<Record<string, unknown>>, activ
if (y < display.workArea.y) { y = cursorPos.y + 20 }
win.setBounds({ x, y, width: popupWidth, height: popupHeight })
win.webContents.send(IPC_CHANNELS.POPUP_COMMAND.SHOW_ITEMS, { commands, activeId })
win.webContents.send(IPC_CHANNELS.POPUP_COMMAND.SHOW_ITEMS, { commands, activeId, _i18n: getPopupI18nStrings() })
if (!win.isVisible()) { win.showInactive() }
win.webContents.send(IPC_CHANNELS.POPUP_COMMAND.SHOW, {})

View file

@ -17,6 +17,9 @@
var linesContainer = document.getElementById('lines')
var container = document.getElementById('container')
// ── i18n ──────────────────────────────────────────────
var i18nStrings = {}
// ── 상태 ──────────────────────────────────────────────
/** @type {Array<{el: HTMLElement, timer: number|null, id: string}>} */
var lines = []
@ -170,13 +173,14 @@
// 상태 변경
window.popupAPI.on('caption:stateChanged', function (data) {
if (data._i18n) i18nStrings = data._i18n
if (data.state === 'starting') {
// 로딩 표시
clearAllLines()
var loadingEl = document.createElement('div')
loadingEl.className = 'caption-line loading'
loadingEl.id = 'caption-loading'
loadingEl.textContent = '⏳ Loading STT model...'
loadingEl.textContent = i18nStrings.captionLoading || '⏳ Loading STT model...'
loadingEl.style.fontSize = config.fontSize + 'px'
linesContainer.appendChild(loadingEl)
} else if (data.state === 'active') {

View file

@ -9,13 +9,13 @@
<body>
<div id="root">
<div id="container" class="command-popup">
<div id="title" class="title">COMMAND SELECT</div>
<div id="title" class="title" data-i18n="commandSelect">COMMAND SELECT</div>
<div id="items" class="items"></div>
<div class="hints">
<span>↑↓ 선택</span>
<span>⏎ 적용</span>
<span>0 해제</span>
<span>ESC ×</span>
<span data-i18n="hintsSelect">↑↓ 선택</span>
<span data-i18n="hintsApply">⏎ 적용</span>
<span data-i18n="hintsClear">0 해제</span>
<span data-i18n="hintsClose">ESC ×</span>
</div>
</div>
</div>

View file

@ -13,6 +13,17 @@
var currentActiveId = null
var i18nStrings = {}
// ── i18n 적용 (HTML 텍스트) ───────────────────────────
function applyI18nToHtml() {
var elements = document.querySelectorAll('[data-i18n]')
elements.forEach(function(el) {
var key = el.getAttribute('data-i18n')
if (i18nStrings[key]) {
el.textContent = i18nStrings[key]
}
})
}
// ── 아이템 렌더링 ───────────────────────────────────
function renderItems(commands, activeId) {
currentCommands = commands
@ -135,6 +146,7 @@
window.popupAPI.on('command:showItems', function (data) {
selectedIndex = 0
if (data._i18n) i18nStrings = data._i18n
applyI18nToHtml()
renderItems(data.commands || [], data.activeId || null)
container.classList.remove('hiding')
})

View file

@ -11,9 +11,9 @@
<div id="container" class="history-popup">
<div id="items" class="items"></div>
<div class="hints">
<span>↑↓ Select</span>
<span>⏎ Paste</span>
<span>ESC ×</span>
<span data-i18n="hintsSelectEn">↑↓ Select</span>
<span data-i18n="hintsPaste">⏎ Paste</span>
<span data-i18n="hintsClose">ESC ×</span>
</div>
</div>
</div>

View file

@ -10,18 +10,30 @@
var items = []
var selectedIndex = 0
var currentEntries = []
var i18nStrings = {}
// ── i18n 적용 (HTML 텍스트) ───────────────────────────
function applyI18nToHtml() {
var elements = document.querySelectorAll('[data-i18n]')
elements.forEach(function(el) {
var key = el.getAttribute('data-i18n')
if (i18nStrings[key]) {
el.textContent = i18nStrings[key]
}
})
}
// ── 상대 시간 표시 ───────────────────────────────────
function relativeTime(timestamp) {
var diff = Date.now() - timestamp
var sec = Math.floor(diff / 1000)
if (sec < 60) return 'just now'
if (sec < 60) return i18nStrings.timeJustNow || 'just now'
var min = Math.floor(sec / 60)
if (min < 60) return min + 'm ago'
if (min < 60) return (i18nStrings.timeMinutesAgo || '{{m}}m ago').replace('{{m}}', min)
var hr = Math.floor(min / 60)
if (hr < 24) return hr + 'h ago'
if (hr < 24) return (i18nStrings.timeHoursAgo || '{{h}}h ago').replace('{{h}}', hr)
var day = Math.floor(hr / 24)
return day + 'd ago'
return (i18nStrings.timeDaysAgo || '{{d}}d ago').replace('{{d}}', day)
}
// ── 텍스트 truncate ──────────────────────────────────
@ -39,7 +51,7 @@
if (entries.length === 0) {
var empty = document.createElement('div')
empty.className = 'empty-state'
empty.textContent = 'No history yet'
empty.textContent = i18nStrings.noHistory || 'No history yet'
itemsContainer.appendChild(empty)
return
}
@ -119,6 +131,8 @@
// ── IPC 리스너 ───────────────────────────────────────
window.popupAPI.on('history:showItems', function (data) {
selectedIndex = 0
if (data._i18n) i18nStrings = data._i18n
applyI18nToHtml()
renderItems(data.entries || [])
container.classList.remove('hiding')
measureAndReport()

View file

@ -42,6 +42,7 @@
var thinkingStartTime = 0
var thinkingRaf = null
var currentState = 'idle'
var i18nStrings = {}
// ── 웨이브 바 생성 ───────────────────────────────────
function createWaveBars() {
@ -133,7 +134,7 @@
currentState = 'error'
hideAllViews()
errorView.classList.remove('hidden')
errorText.textContent = message || '오류가 발생했습니다'
errorText.textContent = message || i18nStrings.errorDefault || '오류가 발생했습니다'
}
// ── 크기 측정 (2-phase 리사이즈) ────────────────────
@ -153,6 +154,7 @@
function setupListeners() {
// Phase 1: prepare — 숨겨진 상태에서 렌더링 후 크기 측정
window.popupAPI.on('window:tipPrepare', function (data) {
if (data._i18n) i18nStrings = data._i18n
var state = data.state
if (state === 'recording') showRecording()
else if (state === 'thinking') showThinking()
@ -184,6 +186,7 @@
// 상태 변경 (showRecordingTip + updateRecordingTipState 양쪽에서 사용)
window.popupAPI.on('window:tipStateChanged', function (data) {
if (data._i18n) i18nStrings = data._i18n
var state = data.state
if (state === 'recording') showRecording()
else if (state === 'thinking') showThinking()

View file

@ -64,16 +64,16 @@ body {
min-height: 2px;
}
/* 9바 시안→블루→퍼플 스펙트럼 (v2 Midnight — RAW.dark gradient.wave1~3 보간) */
.wave-bar:nth-child(1) { background: #22d3ee; box-shadow: 0 0 6px rgba(34, 211, 238, 0.35); }
.wave-bar:nth-child(2) { background: #28bef0; box-shadow: 0 0 6px rgba(40, 190, 240, 0.35); }
.wave-bar:nth-child(3) { background: #2fa9f2; box-shadow: 0 0 6px rgba(47, 169, 242, 0.35); }
.wave-bar:nth-child(4) { background: #3596f4; box-shadow: 0 0 6px rgba(53, 150, 244, 0.35); }
.wave-bar:nth-child(5) { background: #3b82f6; box-shadow: 0 0 6px rgba(59, 130, 246, 0.35); }
.wave-bar:nth-child(6) { background: #4f78f6; box-shadow: 0 0 6px rgba(79, 120, 246, 0.35); }
.wave-bar:nth-child(7) { background: #636ff6; box-shadow: 0 0 6px rgba(99, 111, 246, 0.35); }
.wave-bar:nth-child(8) { background: #7765f6; box-shadow: 0 0 6px rgba(119, 101, 246, 0.35); }
.wave-bar:nth-child(9) { background: #8b5cf6; box-shadow: 0 0 6px rgba(139, 92, 246, 0.35); }
/* 9바 시안→블루→퍼플 스펙트럼 (v2 Midnight — 테마 변수 사용) */
.wave-bar:nth-child(1) { background: var(--d3-wave-1); box-shadow: 0 0 6px var(--d3-wave-1); opacity: 0.9; }
.wave-bar:nth-child(2) { background: var(--d3-wave-2); box-shadow: 0 0 6px var(--d3-wave-2); opacity: 0.92; }
.wave-bar:nth-child(3) { background: var(--d3-wave-3); box-shadow: 0 0 6px var(--d3-wave-3); opacity: 0.94; }
.wave-bar:nth-child(4) { background: var(--d3-wave-4); box-shadow: 0 0 6px var(--d3-wave-4); opacity: 0.96; }
.wave-bar:nth-child(5) { background: var(--d3-wave-5); box-shadow: 0 0 6px var(--d3-wave-5); opacity: 0.98; }
.wave-bar:nth-child(6) { background: var(--d3-wave-6); box-shadow: 0 0 6px var(--d3-wave-6); opacity: 0.98; }
.wave-bar:nth-child(7) { background: var(--d3-wave-7); box-shadow: 0 0 6px var(--d3-wave-7); opacity: 0.96; }
.wave-bar:nth-child(8) { background: var(--d3-wave-8); box-shadow: 0 0 6px var(--d3-wave-8); opacity: 0.94; }
.wave-bar:nth-child(9) { background: var(--d3-wave-9); box-shadow: 0 0 6px var(--d3-wave-9); opacity: 0.92; }
.duration {
color: var(--d3-text-primary);
@ -111,7 +111,7 @@ body {
justify-content: center;
width: 18px;
height: 18px;
background: #ef4444;
background: var(--d3-status-error);
color: white;
border-radius: 50%;
font-size: 12px;

View file

@ -11,7 +11,7 @@
<div id="container" class="result-popup">
<div id="result-text" class="result-text"></div>
<div id="actions" class="actions">
<button id="copy-btn" class="action-btn" title="복사">
<button id="copy-btn" class="action-btn" data-i18n-title="copy" title="복사">
<svg id="copy-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>

View file

@ -9,11 +9,23 @@
var copyBtn = document.getElementById('copy-btn')
var copyIcon = document.getElementById('copy-icon')
var checkIcon = document.getElementById('check-icon')
var i18nStrings = {}
var autoCloseTimer = null
var remainingTime = 0
var lastTick = 0
// ── i18n 적용 (HTML 속성) ───────────────────────────
function applyI18nToHtml() {
var elements = document.querySelectorAll('[data-i18n-title]')
elements.forEach(function(el) {
var key = el.getAttribute('data-i18n-title')
if (i18nStrings[key]) {
el.setAttribute('title', i18nStrings[key])
}
})
}
// ── Auto-close 제어 ─────────────────────────────────
function startAutoCloseTimer(ms) {
remainingTime = ms
@ -64,6 +76,8 @@
// Phase 1: prepare — 결과 텍스트 세팅 + 크기 측정
window.popupAPI.on('result:prepare', function (data) {
resultText.textContent = data.text || ''
if (data._i18n) i18nStrings = data._i18n
applyI18nToHtml()
container.classList.remove('visible')
requestAnimationFrame(function () {

View file

@ -90,7 +90,7 @@ body {
}
.action-btn.copied {
color: #34d399;
color: var(--d3-status-success);
}
.hidden {

View file

@ -226,3 +226,21 @@ export function useI18n(): I18nContextValue {
export function isValidLocale(value: string): value is Locale {
return value in LOCALE_MAP
}
// ── Main 프로세스용 i18n (React 없음) ───────────────────
/**
* Electron main i18n .
* React Context map을 .
* @param locale ( 'ko')
* @returns map (t: (key) => translated string)
*/
export function getI18n(locale: Locale = 'ko'): {
t: (key: TranslationKey, params?: Record<string, string | number>) => string
locale: Locale
} {
const translations = LOCALE_MAP[locale] ?? ko
const t = (key: TranslationKey, params?: Record<string, string | number>): string => {
return resolveTranslation(key, translations, params)
}
return { t, locale }
}

View file

@ -553,5 +553,23 @@
"onboarding.sttModelSize": "Size: ~1.6GB — takes 210 min depending on network speed",
"onboarding.step": "Step {{current}}/{{total}}",
"onboarding.progressDetail": "{{downloaded}} / {{total}} MB",
"onboarding.ollamaNotRunning": "Ollama server is not running. Please retry shortly."
"onboarding.ollamaNotRunning": "Ollama server is not running. Please retry shortly.",
"popup.commandSelect": "COMMAND SELECT",
"popup.hints.select": "↑↓ Select",
"popup.hints.apply": "⏎ Apply",
"popup.hints.clear": "0 Clear",
"popup.hints.close": "ESC ×",
"popup.noCommands": "No commands",
"popup.noCommand": "No command (insert original)",
"popup.hints.selectEn": "↑↓ Select",
"popup.hints.paste": "⏎ Paste",
"popup.noHistory": "No history yet",
"popup.time.justNow": "just now",
"popup.time.minutesAgo": "{{m}}m ago",
"popup.time.hoursAgo": "{{h}}h ago",
"popup.time.daysAgo": "{{d}}d ago",
"popup.copy": "Copy",
"popup.error.default": "An error occurred",
"popup.caption.loading": "⏳ Loading STT model..."
}

View file

@ -553,5 +553,23 @@
"onboarding.sttModelSize": "크기: 약 1.6GB — 네트워크 속도에 따라 2~10분 소요",
"onboarding.step": "단계 {{current}}/{{total}}",
"onboarding.progressDetail": "{{downloaded}} / {{total}} MB",
"onboarding.ollamaNotRunning": "Ollama 서버가 실행되지 않았습니다. 잠시 후 다시 시도하세요."
"onboarding.ollamaNotRunning": "Ollama 서버가 실행되지 않았습니다. 잠시 후 다시 시도하세요.",
"popup.commandSelect": "명령어 선택",
"popup.hints.select": "↑↓ 선택",
"popup.hints.apply": "⏎ 적용",
"popup.hints.clear": "0 해제",
"popup.hints.close": "ESC ×",
"popup.noCommands": "명령어 없음",
"popup.noCommand": "없음 (원본 삽입)",
"popup.hints.selectEn": "↑↓ Select",
"popup.hints.paste": "⏎ Paste",
"popup.noHistory": "히스토리 없음",
"popup.time.justNow": "방금",
"popup.time.minutesAgo": "{{m}}분 전",
"popup.time.hoursAgo": "{{h}}시간 전",
"popup.time.daysAgo": "{{d}}일 전",
"popup.copy": "복사",
"popup.error.default": "오류가 발생했습니다",
"popup.caption.loading": "⏳ STT 모델 로딩 중..."
}

View file

@ -45,6 +45,20 @@ interface PopupThemeVars {
'--d3-action-btn-hover-bg': string
/** 결과 팝업 버튼 hover 색상 */
'--d3-action-btn-hover': string
/** recording-tip 9단계 wave-bar 색상 (wave1~4 보간) */
'--d3-wave-1': string
'--d3-wave-2': string
'--d3-wave-3': string
'--d3-wave-4': string
'--d3-wave-5': string
'--d3-wave-6': string
'--d3-wave-7': string
'--d3-wave-8': string
'--d3-wave-9': string
/** 에러 상태 색상 (recording-tip error-icon) */
'--d3-status-error': string
/** 성공 상태 색상 (result-popup copied 상태) */
'--d3-status-success': string
}
const POPUP_THEME_VARS: Record<PopupThemeKey, PopupThemeVars> = {
@ -68,6 +82,18 @@ const POPUP_THEME_VARS: Record<PopupThemeKey, PopupThemeVars> = {
'--d3-action-btn': '#60a5fa',
'--d3-action-btn-hover-bg': 'rgba(59,130,246,0.14)',
'--d3-action-btn-hover': '#93a4c8',
// wave-bar 9단계: theme.ts RAW.dark.gradient.wave1-4 보간
'--d3-wave-1': '#22d3ee',
'--d3-wave-2': '#28bef0',
'--d3-wave-3': '#2fa9f2',
'--d3-wave-4': '#3596f4',
'--d3-wave-5': '#3b82f6',
'--d3-wave-6': '#4f78f6',
'--d3-wave-7': '#636ff6',
'--d3-wave-8': '#7765f6',
'--d3-wave-9': '#8b5cf6',
'--d3-status-error': '#ef4444',
'--d3-status-success': '#34d399',
},
light: {
'--d3-bg-card': '#ffffff',
@ -89,6 +115,18 @@ const POPUP_THEME_VARS: Record<PopupThemeKey, PopupThemeVars> = {
'--d3-action-btn': '#3b82f6',
'--d3-action-btn-hover-bg': 'rgba(37,99,235,0.10)',
'--d3-action-btn-hover': '#4b5a75',
// theme.ts RAW.light.gradient.wave1-4 보간
'--d3-wave-1': '#06b6d4',
'--d3-wave-2': '#1cbff8',
'--d3-wave-3': '#32bbfc',
'--d3-wave-4': '#3b82f6',
'--d3-wave-5': '#4966eb',
'--d3-wave-6': '#574ae0',
'--d3-wave-7': '#6c2edd',
'--d3-wave-8': '#8112da',
'--d3-wave-9': '#d946ef',
'--d3-status-error': '#ef4444',
'--d3-status-success': '#34d399',
},
nord: {
'--d3-bg-card': '#3b4252',
@ -110,6 +148,18 @@ const POPUP_THEME_VARS: Record<PopupThemeKey, PopupThemeVars> = {
'--d3-action-btn': 'rgba(216,222,233,0.4)',
'--d3-action-btn-hover-bg': 'rgba(216,222,233,0.08)',
'--d3-action-btn-hover': 'rgba(216,222,233,0.7)',
// theme.ts RAW.nord.gradient.wave1-4 보간
'--d3-wave-1': '#8fbcbb',
'--d3-wave-2': '#8dbdcd',
'--d3-wave-3': '#8bbea0',
'--d3-wave-4': '#88c0d0',
'--d3-wave-5': '#85c6d0',
'--d3-wave-6': '#a3be8c',
'--d3-wave-7': '#b48ead',
'--d3-wave-8': '#bf88a8',
'--d3-wave-9': '#d08770',
'--d3-status-error': '#bf616a',
'--d3-status-success': '#a3be8c',
},
solarized: {
'--d3-bg-card': '#073642',
@ -131,6 +181,18 @@ const POPUP_THEME_VARS: Record<PopupThemeKey, PopupThemeVars> = {
'--d3-action-btn': 'rgba(147,161,161,0.4)',
'--d3-action-btn-hover-bg': 'rgba(131,148,150,0.10)',
'--d3-action-btn-hover': 'rgba(147,161,161,0.7)',
// theme.ts RAW.solarized.gradient.wave1-4 보간
'--d3-wave-1': '#2aa198',
'--d3-wave-2': '#2693cb',
'--d3-wave-3': '#268bd2',
'--d3-wave-4': '#268bd2',
'--d3-wave-5': '#4676d9',
'--d3-wave-6': '#6c71c4',
'--d3-wave-7': '#6c71c4',
'--d3-wave-8': '#9a33d3',
'--d3-wave-9': '#d33682',
'--d3-status-error': '#dc322f',
'--d3-status-success': '#859900',
},
catppuccin: {
'--d3-bg-card': '#313244',
@ -152,6 +214,18 @@ const POPUP_THEME_VARS: Record<PopupThemeKey, PopupThemeVars> = {
'--d3-action-btn': 'rgba(166,173,200,0.4)',
'--d3-action-btn-hover-bg': 'rgba(205,214,244,0.06)',
'--d3-action-btn-hover': 'rgba(205,214,244,0.7)',
// theme.ts RAW.catppuccin.gradient.wave1-4 보간
'--d3-wave-1': '#89dceb',
'--d3-wave-2': '#89cff4',
'--d3-wave-3': '#89c2f9',
'--d3-wave-4': '#89b4fa',
'--d3-wave-5': '#b4a9f2',
'--d3-wave-6': '#df9eea',
'--d3-wave-7': '#cba6f7',
'--d3-wave-8': '#e5b6f6',
'--d3-wave-9': '#f5c2e7',
'--d3-status-error': '#f38ba8',
'--d3-status-success': '#a6e3a1',
},
dracula: {
'--d3-bg-card': '#44475a',
@ -173,6 +247,18 @@ const POPUP_THEME_VARS: Record<PopupThemeKey, PopupThemeVars> = {
'--d3-action-btn': 'rgba(169,176,208,0.4)',
'--d3-action-btn-hover-bg': 'rgba(248,248,242,0.06)',
'--d3-action-btn-hover': 'rgba(248,248,242,0.7)',
// theme.ts RAW.dracula.gradient.wave1-4 보간
'--d3-wave-1': '#8be9fd',
'--d3-wave-2': '#a6fbff',
'--d3-wave-3': '#c1fdff',
'--d3-wave-4': '#bd93f9',
'--d3-wave-5': '#d485ff',
'--d3-wave-6': '#eb77ff',
'--d3-wave-7': '#ff79c6',
'--d3-wave-8': '#ff88c4',
'--d3-wave-9': '#ffb86c',
'--d3-status-error': '#ff5555',
'--d3-status-success': '#50fa7b',
},
}