""" 커스텀 모달 컴포넌트 (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 = """ """ # 모달 HTML 템플릿 - visible이 true일 때만 렌더링 # 다크모드 감지: document.body나 .gradio-container에 .dark 클래스가 있는지 확인 HTML_TEMPLATE = """ ${value && value.visible ? `
` : ''} """ # 모달 CSS 스타일 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; } """ # 모달 JavaScript (이벤트 핸들링) 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": ""}