앱 위젯추가 팝업 수정

This commit is contained in:
Macbook
2026-06-15 16:30:44 +09:00
parent 1eaecbb586
commit 63a40c9c75
4 changed files with 223 additions and 50 deletions
+14 -2
View File
@@ -137,7 +137,7 @@ const WidgetApp: React.FC = () => {
const sizeBeforePickerRef = useRef<{ width: number; height: number } | null>(null); const sizeBeforePickerRef = useRef<{ width: number; height: number } | null>(null);
const handlePickerOpenChange = useCallback(async (open: boolean) => { const handlePickerOpenChange = useCallback(async (open: boolean): Promise<void> => {
const win = getCurrentWindow(); const win = getCurrentWindow();
if (open) { if (open) {
const size = await win.innerSize(); const size = await win.innerSize();
@@ -148,6 +148,18 @@ const WidgetApp: React.FC = () => {
const h = Math.min(NATIVE_WIDGET_PICKER_WINDOW.height, maxH); const h = Math.min(NATIVE_WIDGET_PICKER_WINDOW.height, maxH);
await win.setSize(new LogicalSize(w, h)); await win.setSize(new LogicalSize(w, h));
await win.center(); await win.center();
await new Promise<void>(resolve => {
let settled = false;
const finish = () => {
if (settled) return;
settled = true;
unlisten?.();
resolve();
};
let unlisten: (() => void) | undefined;
void win.onResized(finish).then(fn => { unlisten = fn; });
window.setTimeout(finish, 150);
});
return; return;
} }
const prev = sizeBeforePickerRef.current; const prev = sizeBeforePickerRef.current;
@@ -183,7 +195,7 @@ const WidgetApp: React.FC = () => {
nativeShell nativeShell
onClose={handleClose} onClose={handleClose}
onFocus={() => {}} onFocus={() => {}}
onPickerOpenChange={open => { void handlePickerOpenChange(open); }} onPickerOpenChange={handlePickerOpenChange}
onUpdateSlots={slots => setInstance(prev => (prev ? { ...prev, slots } : prev))} onUpdateSlots={slots => setInstance(prev => (prev ? { ...prev, slots } : prev))}
onUpdateSize={(width, height) => { onUpdateSize={(width, height) => {
setInstance(prev => (prev ? { ...prev, width, height } : prev)); setInstance(prev => (prev ? { ...prev, width, height } : prev));
@@ -24,8 +24,8 @@ interface Props {
onUpdateGridFr: (rowFr: number[], colFr: number[]) => void; onUpdateGridFr: (rowFr: number[], colFr: number[]) => void;
/** Tauri OS 창 — 네이티브 타이틀·리사이즈 사용, 내부 플로팅 크롬 숨김 */ /** Tauri OS 창 — 네이티브 타이틀·리사이즈 사용, 내부 플로팅 크롬 숨김 */
nativeShell?: boolean; nativeShell?: boolean;
/** 위젯 선택 팝업 열림/닫힘 (네이티브 창 크기 조절) */ /** 위젯 선택 팝업 열림/닫힘 (네이티브 창 크기 조절 — 완료 후 picker 표시) */
onPickerOpenChange?: (open: boolean) => void; onPickerOpenChange?: (open: boolean) => void | Promise<void>;
} }
const FloatingWidgetWindow: React.FC<Props> = ({ const FloatingWidgetWindow: React.FC<Props> = ({
@@ -40,16 +40,29 @@ const FloatingWidgetWindow: React.FC<Props> = ({
onPickerOpenChange, onPickerOpenChange,
}) => { }) => {
const [pickSlotId, setPickSlotId] = useState<string | null>(null); const [pickSlotId, setPickSlotId] = useState<string | null>(null);
const [pickerOpening, setPickerOpening] = useState(false);
const openPicker = useCallback((slotId: string) => { const openPicker = useCallback(async (slotId: string) => {
if (nativeShell && onPickerOpenChange) {
setPickerOpening(true);
try {
await onPickerOpenChange(true);
await new Promise<void>(resolve => {
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
});
} finally {
setPickerOpening(false);
}
}
setPickSlotId(slotId); setPickSlotId(slotId);
onPickerOpenChange?.(true); }, [nativeShell, onPickerOpenChange]);
}, [onPickerOpenChange]);
const closePicker = useCallback(() => { const closePicker = useCallback(async () => {
setPickSlotId(null); setPickSlotId(null);
onPickerOpenChange?.(false); if (nativeShell && onPickerOpenChange) {
}, [onPickerOpenChange]); await onPickerOpenChange(false);
}
}, [nativeShell, onPickerOpenChange]);
const bodyWrapRef = useRef<HTMLDivElement>(null); const bodyWrapRef = useRef<HTMLDivElement>(null);
const bodyRef = useRef<HTMLDivElement>(null); const bodyRef = useRef<HTMLDivElement>(null);
const [bodySize, setBodySize] = useState({ width: 0, height: 0 }); const [bodySize, setBodySize] = useState({ width: 0, height: 0 });
@@ -143,8 +156,7 @@ const FloatingWidgetWindow: React.FC<Props> = ({
const applyWidgetType = useCallback((widgetType: string) => { const applyWidgetType = useCallback((widgetType: string) => {
if (!pickSlotId) return; if (!pickSlotId) return;
updateSlot(pickSlotId, s => ({ ...s, widgetType, config: {} })); updateSlot(pickSlotId, s => ({ ...s, widgetType, config: {} }));
closePicker(); }, [pickSlotId, updateSlot]);
}, [pickSlotId, updateSlot, closePicker]);
const clearWidget = useCallback((slotId: string) => { const clearWidget = useCallback((slotId: string) => {
updateSlot(slotId, s => ({ ...s, widgetType: null, config: {} })); updateSlot(slotId, s => ({ ...s, widgetType: null, config: {} }));
@@ -246,7 +258,7 @@ const FloatingWidgetWindow: React.FC<Props> = ({
<WidgetSlotView <WidgetSlotView
slot={slot} slot={slot}
zoneId="center" zoneId="center"
onPickWidget={openPicker} onPickWidget={id => { void openPicker(id); }}
onClearWidget={id => clearWidget(id)} onClearWidget={id => clearWidget(id)}
/> />
</div> </div>
@@ -301,13 +313,18 @@ const FloatingWidgetWindow: React.FC<Props> = ({
)} )}
</div> </div>
<WidgetPickerModal {(pickerOpening || pickSlotId != null) && (
open={pickSlotId != null} <WidgetPickerModal
category="all" open={pickSlotId != null}
nativeCompact={nativeShell} category="all"
onClose={closePicker} nativeCompact={nativeShell}
onSelect={applyWidgetType} onClose={() => { void closePicker(); }}
/> onSelect={applyWidgetType}
/>
)}
{pickerOpening && pickSlotId == null && nativeShell && (
<div className="wd-picker-native-loading" aria-live="polite"> </div>
)}
</> </>
); );
@@ -10,20 +10,11 @@ interface Props {
category: WidgetPickerCategory; category: WidgetPickerCategory;
onClose: () => void; onClose: () => void;
onSelect: (widgetType: string) => void; onSelect: (widgetType: string) => void;
/** Tauri 네이티브 위젯 창 — 작은 viewport에 맞춘 전체 화면형 팝업 */ /** Tauri 네이티브 위젯 창 — OS 창 전체를 쓰는 고정형 picker */
nativeCompact?: boolean; nativeCompact?: boolean;
} }
const WidgetPickerModal: React.FC<Props> = ({ function pickerMeta(category: WidgetPickerCategory) {
open,
category,
onClose,
onSelect,
nativeCompact = false,
}) => {
if (!open) return null;
const groups = category === 'all' ? widgetsByGroupAll() : widgetsByGroup(category);
const title = category === 'center' const title = category === 'center'
? '메인 위젯' ? '메인 위젯'
: category === 'side' : category === 'side'
@@ -34,20 +25,19 @@ const WidgetPickerModal: React.FC<Props> = ({
: category === 'side' : category === 'side'
? '좌·우·하단 패널에 추가할 위젯을 선택하세요. 주식 HTS/위젯 프로그램에서 흔히 쓰는 구성을 지원합니다.' ? '좌·우·하단 패널에 추가할 위젯을 선택하세요. 주식 HTS/위젯 프로그램에서 흔히 쓰는 구성을 지원합니다.'
: '추가할 위젯 유형을 선택하세요. 차트·시세·매매·포트폴리오 등 모든 위젯을 사용할 수 있습니다.'; : '추가할 위젯 유형을 선택하세요. 차트·시세·매매·포트폴리오 등 모든 위젯을 사용할 수 있습니다.';
return { title, intro };
}
const WidgetPickerContent: React.FC<{
category: WidgetPickerCategory;
onSelect: (widgetType: string) => void;
onClose: () => void;
}> = ({ category, onSelect, onClose }) => {
const groups = category === 'all' ? widgetsByGroupAll() : widgetsByGroup(category);
const { intro } = pickerMeta(category);
return ( return (
<AppPopup <>
onClose={onClose}
title={title}
titleKo="Widget"
width={720}
maxWidth="96vw"
backdrop
closeOnBackdrop
zIndex={13200}
shellClassName={nativeCompact ? 'app-popup-shell--widget-picker-native' : 'app-popup-shell--widget-picker'}
bodyClassName="wd-picker-popup-body"
>
<p className="wd-layout-intro">{intro}</p> <p className="wd-layout-intro">{intro}</p>
{groups.map(g => ( {groups.map(g => (
<section key={g.group} className="wd-picker-section"> <section key={g.group} className="wd-picker-section">
@@ -70,6 +60,85 @@ const WidgetPickerModal: React.FC<Props> = ({
</div> </div>
</section> </section>
))} ))}
</>
);
};
const NativeWidgetPicker: React.FC<Props> = ({
open,
category,
onClose,
onSelect,
}) => {
if (!open) return null;
const { title } = pickerMeta(category);
return (
<div className="wd-picker-native-overlay" role="presentation">
<div
className="wd-picker-native-shell"
role="dialog"
aria-modal="true"
aria-labelledby="wd-picker-native-title"
>
<div className="wd-picker-native-header">
<h2 id="wd-picker-native-title" className="wd-picker-native-title">
{title}
<span className="wd-picker-native-title-ko"> (Widget)</span>
</h2>
<button
type="button"
className="wd-picker-native-close"
onClick={onClose}
aria-label="닫기"
>
</button>
</div>
<div className="wd-picker-native-body wd-picker-popup-body">
<WidgetPickerContent category={category} onSelect={onSelect} onClose={onClose} />
</div>
</div>
</div>
);
};
const WidgetPickerModal: React.FC<Props> = ({
open,
category,
onClose,
onSelect,
nativeCompact = false,
}) => {
if (!open) return null;
if (nativeCompact) {
return (
<NativeWidgetPicker
open={open}
category={category}
onClose={onClose}
onSelect={onSelect}
/>
);
}
const { title } = pickerMeta(category);
return (
<AppPopup
onClose={onClose}
title={title}
titleKo="Widget"
width={720}
maxWidth="96vw"
backdrop
closeOnBackdrop
zIndex={13200}
shellClassName="app-popup-shell--widget-picker"
bodyClassName="wd-picker-popup-body"
>
<WidgetPickerContent category={category} onSelect={onSelect} onClose={onClose} />
</AppPopup> </AppPopup>
); );
}; };
+82 -7
View File
@@ -1241,17 +1241,92 @@
max-height: none; max-height: none;
} }
/* Tauri 네이티브 위젯 창 — OS확대 후 팝업이 거의 전체를 채움 */ /* Tauri 네이티브 위젯 창 — 전체picker (AppPopup draggable 미사용) */
html.fw-native-widget .app-popup-shell--widget-picker-native { html.fw-native-widget .wd-picker-native-overlay {
width: calc(100vw - 12px) !important; position: fixed;
max-width: calc(100vw - 12px) !important; inset: 0;
max-height: calc(100vh - 12px); z-index: 13200;
min-height: calc(100vh - 16px); display: flex;
flex-direction: column;
padding: 10px;
box-sizing: border-box;
background: rgba(0, 0, 0, 0.5);
} }
html.fw-native-widget .app-popup-shell--widget-picker-native .wd-picker-popup-body { html.fw-native-widget .wd-picker-native-shell {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
display: flex;
flex-direction: column;
border-radius: 12px;
border: 1px solid color-mix(in srgb, var(--accent, #3f7ef5) 45%, var(--border));
background: var(--bg2, #1e222d);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.55);
overflow: hidden;
}
html.fw-native-widget .wd-picker-native-header {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 10px 14px;
border-bottom: 1px solid var(--border);
background: linear-gradient(
180deg,
color-mix(in srgb, var(--accent, #3f7ef5) 14%, var(--bg2)) 0%,
var(--bg2, #1e222d) 100%
);
}
html.fw-native-widget .wd-picker-native-title {
margin: 0;
font-size: 14px;
font-weight: 700;
color: var(--text);
}
html.fw-native-widget .wd-picker-native-title-ko {
font-size: 12px;
font-weight: 400;
color: var(--text2);
}
html.fw-native-widget .wd-picker-native-close {
flex-shrink: 0;
width: 28px;
height: 28px;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--bg3);
color: var(--text2);
cursor: pointer;
font-size: 14px;
line-height: 1;
}
html.fw-native-widget .wd-picker-native-close:hover {
color: var(--text);
border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
}
html.fw-native-widget .wd-picker-native-body {
flex: 1;
min-height: 0;
overflow-y: auto;
}
html.fw-native-widget .wd-picker-native-loading {
position: fixed;
inset: 0;
z-index: 13199;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.35);
color: var(--text2);
font-size: 13px;
} }
.bps-page--wd-no-left .bps-side-wrap--left, .bps-page--wd-no-left .bps-side-wrap--left,