diff --git a/src/renderer/handlers/hover-preview.js b/src/renderer/handlers/hover-preview.js index 18f0ca2..17d130d 100644 --- a/src/renderer/handlers/hover-preview.js +++ b/src/renderer/handlers/hover-preview.js @@ -1,9 +1,14 @@ const DELAY = 1000; -let _el = null; -let _imgEl = null; -let _timer = null; -let _visible = false; +const SVG_NS = 'http://www.w3.org/2000/svg'; +const RADIUS = 20; +const CIRC = 2 * Math.PI * RADIUS; // ~125.66 + +let _el = null; +let _imgEl = null; +let _timer = null; +let _visible = false; +let _loaderEl = null; export function init() { _el = document.createElement('div'); @@ -22,7 +27,10 @@ export function setupCard(card) { function _onEnter() { const card = this; clearTimeout(_timer); + _showLoader(card); + _timer = setTimeout(() => { + _removeLoader(); const thumb = card.querySelector('img.card-thumb'); if (!thumb) return; _imgEl.src = thumb.src; @@ -35,12 +43,60 @@ function _onLeave() { clearTimeout(_timer); _timer = null; _visible = false; - _el.style.display = 'none'; - _el.style.visibility = 'hidden'; + _removeLoader(); + _el.style.display = 'none'; + _el.style.visibility = 'hidden'; } +// ── 원형 로딩바 ────────────────────────────────── + +function _showLoader(card) { + _removeLoader(); + + const svg = document.createElementNS(SVG_NS, 'svg'); + svg.classList.add('card-hover-loader'); + svg.setAttribute('viewBox', '0 0 48 48'); + + // 배경 원 + const bg = document.createElementNS(SVG_NS, 'circle'); + bg.classList.add('loader-bg'); + bg.setAttribute('cx', '24'); + bg.setAttribute('cy', '24'); + bg.setAttribute('r', '24'); + + // 트랙 링 + const track = document.createElementNS(SVG_NS, 'circle'); + track.classList.add('loader-track'); + track.setAttribute('cx', '24'); + track.setAttribute('cy', '24'); + track.setAttribute('r', String(RADIUS)); + + // 진행 호 + const progress = document.createElementNS(SVG_NS, 'circle'); + progress.classList.add('loader-progress'); + progress.setAttribute('cx', '24'); + progress.setAttribute('cy', '24'); + progress.setAttribute('r', String(RADIUS)); + progress.style.strokeDasharray = CIRC; + progress.style.strokeDashoffset = CIRC; + + svg.appendChild(bg); + svg.appendChild(track); + svg.appendChild(progress); + card.appendChild(svg); + _loaderEl = svg; +} + +function _removeLoader() { + if (_loaderEl) { + _loaderEl.remove(); + _loaderEl = null; + } +} + +// ── 이미지 팝업 ────────────────────────────────── + function _show(card) { - // 위치 계산 전 화면 밖에 렌더링해서 실제 크기를 측정 _el.style.display = 'block'; _el.style.visibility = 'hidden'; _el.style.left = '0px'; @@ -55,14 +111,12 @@ function _show(card) { const vh = window.innerHeight; const gap = 16; - // 가로: 카드 오른쪽 우선, 공간 부족 시 왼쪽 let left = cardRect.right + gap; if (left + popup.width > vw - 8) { left = cardRect.left - popup.width - gap; } left = Math.max(8, Math.min(left, vw - popup.width - 8)); - // 세로: 카드와 중앙 정렬, 화면 경계 클램프 let top = cardRect.top + (cardRect.height - popup.height) / 2; top = Math.max(8, Math.min(top, vh - popup.height - 8)); diff --git a/src/renderer/style.css b/src/renderer/style.css index eab4416..4c595e0 100644 --- a/src/renderer/style.css +++ b/src/renderer/style.css @@ -269,6 +269,40 @@ body { @keyframes spin { to { transform: rotate(360deg); } } #loading p { color: var(--text); font-size: 14px; } +/* ── 카드 호버 원형 로딩바 ── */ +.card-hover-loader { + position: absolute; + top: calc(var(--thumb-size) / 2); + left: 50%; + transform: translate(-50%, -50%); + width: 52px; + height: 52px; + pointer-events: none; + z-index: 10; + overflow: visible; +} +.card-hover-loader .loader-bg { + fill: rgba(0, 0, 0, 0.55); +} +.card-hover-loader .loader-track { + fill: none; + stroke: rgba(255, 255, 255, 0.15); + stroke-width: 3; +} +.card-hover-loader .loader-progress { + fill: none; + stroke: var(--accent); + stroke-width: 3; + stroke-linecap: round; + transform-origin: 24px 24px; + transform: rotate(-90deg); + animation: hover-loader-fill 1s linear forwards; +} +@keyframes hover-loader-fill { + from { stroke-dashoffset: 125.66; } + to { stroke-dashoffset: 0; } +} + /* ── 호버 이미지 팝업 ── */ .hover-preview { position: fixed;