|
|
""" |
|
|
커스텀 모달 컴포넌트 (Gradio 6 호환) |
|
|
순수 gr.HTML 기반 구현 |
|
|
|
|
|
사용법: |
|
|
from frontend.components.custom_modal import Modal |
|
|
|
|
|
with gr.Blocks() as demo: |
|
|
modal = Modal(elem_id="my-modal") |
|
|
modal.render() |
|
|
|
|
|
# 모달 열기 |
|
|
show_btn.click( |
|
|
fn=lambda content: Modal.show(content), |
|
|
outputs=[modal.component] |
|
|
) |
|
|
|
|
|
# 모달 닫기 (자동 - X 버튼, ESC, 배경 클릭) |
|
|
""" |
|
|
|
|
|
import gradio as gr |
|
|
|
|
|
|
|
|
class Modal: |
|
|
""" |
|
|
Gradio 6 호환 커스텀 모달 컴포넌트 |
|
|
|
|
|
gr.HTML의 html_template, css_template, js_on_load를 활용하여 모달 기능 구현 |
|
|
""" |
|
|
|
|
|
backup_button = """ |
|
|
<button class="modal-close" data-modal-close="true"> |
|
|
<svg width="12" height="12" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|
|
<path d="M1 1L9 9" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> |
|
|
<path d="M9 1L1 9" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> |
|
|
</svg> |
|
|
</button>""" |
|
|
|
|
|
|
|
|
|
|
|
HTML_TEMPLATE = """ |
|
|
${value && value.visible ? ` |
|
|
<div class="modal-overlay ${document.body.classList.contains('dark') || document.querySelector('.dark') ? 'modal-dark' : ''}" data-modal-overlay="true"> |
|
|
<div class="modal-container"> |
|
|
|
|
|
<div class="modal-content"> |
|
|
${value.content || ''} |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
` : ''} |
|
|
""" |
|
|
|
|
|
|
|
|
CSS_TEMPLATE = """ |
|
|
.modal-overlay { |
|
|
position: fixed; |
|
|
top: 0; |
|
|
left: 0; |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
background-color: rgba(0, 0, 0, 0.15); |
|
|
backdrop-filter: blur(2px); |
|
|
z-index: 1000; |
|
|
display: flex; |
|
|
justify-content: center; |
|
|
align-items: center; |
|
|
} |
|
|
|
|
|
/* 다크모드: 블러 제거, 반투명 배경만 */ |
|
|
.modal-overlay.modal-dark { |
|
|
background-color: rgba(0, 0, 0, 0.3) !important; |
|
|
} |
|
|
|
|
|
.modal-container { |
|
|
background: var(--background-fill-primary, #ffffff); |
|
|
border-radius: 20px; |
|
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2); |
|
|
width: 92%; |
|
|
min-width: 350px; |
|
|
max-width: 600px; |
|
|
max-height: 95vh; |
|
|
overflow-y: auto; |
|
|
position: relative; |
|
|
animation: modalFadeIn 0.2s ease-out; |
|
|
} |
|
|
|
|
|
/* 다크모드 모달 컨테이너 */ |
|
|
.modal-dark .modal-container { |
|
|
background: #1a1a1b; |
|
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); |
|
|
} |
|
|
|
|
|
.modal-dark .modal-close { |
|
|
background: #3a3a3c; |
|
|
border-color: #4a4a4c; |
|
|
color: #ffffff; |
|
|
} |
|
|
|
|
|
.modal-dark .modal-close:hover { |
|
|
background: #4a4a4c; |
|
|
} |
|
|
|
|
|
@keyframes modalFadeIn { |
|
|
from { |
|
|
opacity: 0; |
|
|
transform: scale(0.95) translateY(-10px); |
|
|
} |
|
|
to { |
|
|
opacity: 1; |
|
|
transform: scale(1) translateY(0); |
|
|
} |
|
|
} |
|
|
|
|
|
.modal-close { |
|
|
position: absolute; |
|
|
top: 12px; |
|
|
right: 12px; |
|
|
background: var(--background-fill-secondary, #f0f0f0); |
|
|
border: 1px solid var(--border-color-primary, #ddd); |
|
|
border-radius: 50%; |
|
|
width: 40px; |
|
|
height: 40px; |
|
|
cursor: pointer; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
color: var(--body-text-color, #333); |
|
|
transition: all 0.2s ease; |
|
|
z-index: 10; |
|
|
padding: 0; |
|
|
} |
|
|
|
|
|
.modal-close:hover { |
|
|
background: var(--background-fill-secondary-hover, #e0e0e0); |
|
|
transition: colors 0.2s ease; |
|
|
} |
|
|
|
|
|
.modal-content { |
|
|
padding: 20px; |
|
|
} |
|
|
""" |
|
|
|
|
|
|
|
|
JS_ON_LOAD = """ |
|
|
// ESC 키로 닫기 |
|
|
const handleKeydown = (e) => { |
|
|
if (e.key === 'Escape' && props.value && props.value.visible) { |
|
|
props.value = { visible: false, content: '' }; |
|
|
} |
|
|
}; |
|
|
document.addEventListener('keydown', handleKeydown); |
|
|
|
|
|
// 클릭 이벤트 (배경 클릭, X 버튼) |
|
|
element.addEventListener('click', (e) => { |
|
|
// 배경 클릭 |
|
|
if (e.target.dataset.modalOverlay === 'true') { |
|
|
props.value = { visible: false, content: '' }; |
|
|
} |
|
|
|
|
|
// X 버튼 클릭 |
|
|
if (e.target.closest('[data-modal-close="true"]')) { |
|
|
props.value = { visible: false, content: '' }; |
|
|
} |
|
|
}); |
|
|
""" |
|
|
|
|
|
def __init__( |
|
|
self, |
|
|
visible: bool = False, |
|
|
content: str = "", |
|
|
elem_id: str | None = None, |
|
|
elem_classes: list[str] | str | None = None, |
|
|
): |
|
|
""" |
|
|
모달 초기화 |
|
|
|
|
|
Args: |
|
|
visible: 모달 표시 여부 |
|
|
content: 모달 내부 HTML 콘텐츠 |
|
|
elem_id: HTML element ID |
|
|
elem_classes: HTML element classes |
|
|
""" |
|
|
self.visible = visible |
|
|
self.content = content |
|
|
self.elem_id = elem_id |
|
|
self.elem_classes = elem_classes |
|
|
self.component = None |
|
|
|
|
|
def render(self) -> gr.HTML: |
|
|
""" |
|
|
모달 컴포넌트 렌더링 |
|
|
|
|
|
Returns: |
|
|
gr.HTML: 렌더링된 모달 컴포넌트 |
|
|
""" |
|
|
self.component = gr.HTML( |
|
|
value={"visible": self.visible, "content": self.content}, |
|
|
html_template=self.HTML_TEMPLATE, |
|
|
css_template=self.CSS_TEMPLATE, |
|
|
js_on_load=self.JS_ON_LOAD, |
|
|
elem_id=self.elem_id, |
|
|
elem_classes=self.elem_classes, |
|
|
) |
|
|
return self.component |
|
|
|
|
|
@staticmethod |
|
|
def show(content: str = "") -> dict: |
|
|
"""모달 열기""" |
|
|
return {"visible": True, "content": content} |
|
|
|
|
|
@staticmethod |
|
|
def hide() -> dict: |
|
|
"""모달 닫기""" |
|
|
return {"visible": False, "content": ""} |
|
|
|