152 lines
4.1 KiB
TypeScript
152 lines
4.1 KiB
TypeScript
/**
|
|
* AppPopup — TradeAlertModal(tam-) 기준 공통 팝업 셸
|
|
* 신규 팝업은 이 컴포넌트를 사용하세요.
|
|
*/
|
|
import '../styles/appPopup.css';
|
|
import React from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
import { useDraggablePanel } from '../hooks/useDraggablePanel';
|
|
|
|
export const APP_POPUP_ACCENT = '#3f7ef5';
|
|
export const APP_POPUP_ACCENT_GLOW = 'rgba(63,126,245,0.22)';
|
|
|
|
export interface AppPopupProps {
|
|
onClose: () => void;
|
|
title: React.ReactNode;
|
|
titleKo?: React.ReactNode;
|
|
badge?: string;
|
|
accentColor?: string;
|
|
children: React.ReactNode;
|
|
width?: number | string;
|
|
maxWidth?: number | string;
|
|
className?: string;
|
|
shellClassName?: string;
|
|
overlayClassName?: string;
|
|
bodyClassName?: string;
|
|
centered?: boolean;
|
|
/** 콘텐츠 크기 변화·창 리사이즈 시 (드래그 전까지) 중앙 재정렬 */
|
|
recenterOnResize?: boolean;
|
|
zIndex?: number;
|
|
closeOnBackdrop?: boolean;
|
|
backdrop?: boolean;
|
|
portal?: boolean;
|
|
headerExtra?: React.ReactNode;
|
|
footer?: React.ReactNode;
|
|
initialPosition?: { x: number; y: number };
|
|
bodyRef?: React.Ref<HTMLDivElement>;
|
|
}
|
|
|
|
export const AppPopup: React.FC<AppPopupProps> = ({
|
|
onClose,
|
|
title,
|
|
titleKo,
|
|
badge,
|
|
accentColor = APP_POPUP_ACCENT,
|
|
children,
|
|
width = 400,
|
|
maxWidth = '96vw',
|
|
className,
|
|
shellClassName,
|
|
overlayClassName,
|
|
bodyClassName = 'app-popup-body',
|
|
centered = true,
|
|
recenterOnResize = false,
|
|
zIndex = 9999,
|
|
closeOnBackdrop = true,
|
|
backdrop = false,
|
|
portal = true,
|
|
headerExtra,
|
|
footer,
|
|
initialPosition,
|
|
bodyRef,
|
|
}) => {
|
|
const accentGlow = accentColor === APP_POPUP_ACCENT
|
|
? APP_POPUP_ACCENT_GLOW
|
|
: `${accentColor}38`;
|
|
|
|
const {
|
|
panelRef,
|
|
dragging,
|
|
onHeaderPointerDown,
|
|
headerTouchStyle,
|
|
panelStyle,
|
|
headerCursor,
|
|
} = useDraggablePanel({ centerOnMount: centered, recenterOnResize, initialPosition });
|
|
|
|
const overlayCls = [
|
|
'app-popup-overlay',
|
|
backdrop ? 'app-popup-overlay--dim' : '',
|
|
overlayClassName ?? '',
|
|
className ?? '',
|
|
].filter(Boolean).join(' ');
|
|
|
|
const shellCls = ['app-popup-shell', shellClassName ?? ''].filter(Boolean).join(' ');
|
|
|
|
const content = (
|
|
<div
|
|
className={overlayCls}
|
|
style={{ zIndex }}
|
|
onMouseDown={e => {
|
|
if (closeOnBackdrop && backdrop && e.target === e.currentTarget) onClose();
|
|
}}
|
|
>
|
|
<div
|
|
ref={panelRef}
|
|
className={shellCls}
|
|
style={{
|
|
...panelStyle,
|
|
width,
|
|
maxWidth,
|
|
cursor: dragging ? 'grabbing' : undefined,
|
|
'--app-popup-accent': accentColor,
|
|
'--app-popup-accent-glow': accentGlow,
|
|
'--tam-accent': accentColor,
|
|
'--tam-accent-glow': accentGlow,
|
|
'--lsp-accent': accentColor,
|
|
'--lsp-accent-glow': accentGlow,
|
|
} as React.CSSProperties}
|
|
onMouseDown={e => e.stopPropagation()}
|
|
>
|
|
<div
|
|
className="gc-popup-header app-popup-header"
|
|
onPointerDown={onHeaderPointerDown}
|
|
style={{ cursor: headerCursor, ...headerTouchStyle }}
|
|
>
|
|
<div className="app-popup-header-left">
|
|
{badge ? (
|
|
<span className="app-popup-badge" style={{ background: accentColor }}>
|
|
{badge}
|
|
</span>
|
|
) : null}
|
|
<span className="app-popup-header-title">
|
|
{title}
|
|
{titleKo != null && titleKo !== '' ? (
|
|
<span className="app-popup-header-ko"> ({titleKo})</span>
|
|
) : null}
|
|
</span>
|
|
</div>
|
|
{headerExtra}
|
|
<button
|
|
type="button"
|
|
className="app-popup-close gc-popup-close"
|
|
onMouseDown={e => e.stopPropagation()}
|
|
onClick={onClose}
|
|
aria-label="닫기"
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
<div ref={bodyRef} className={bodyClassName}>{children}</div>
|
|
{footer}
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
if (portal && typeof document !== 'undefined' && document.body) {
|
|
return createPortal(content, document.body);
|
|
}
|
|
return content;
|
|
};
|
|
|
|
export default AppPopup;
|