fix(desktop): 네이티브 위젯 picker를 창 전체 뷰로 전환
portal/overlay 대신 widget-main에서 picker 전용 화면으로 전환해 macOS·Windows WebView 모두에서 위젯 선택 팝업이 보이도록 합니다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+57
-15
@@ -6,6 +6,7 @@ import { LogicalSize } from '@tauri-apps/api/dpi';
|
|||||||
import { initStorage, refreshApiBaseFromStorage } from '@goldenchart/shared';
|
import { initStorage, refreshApiBaseFromStorage } from '@goldenchart/shared';
|
||||||
import { ensureDesktopApiBase } from './ensureDesktopApiBase';
|
import { ensureDesktopApiBase } from './ensureDesktopApiBase';
|
||||||
import FloatingWidgetWindow from '@frontend/components/floatingWidgets/FloatingWidgetWindow';
|
import FloatingWidgetWindow from '@frontend/components/floatingWidgets/FloatingWidgetWindow';
|
||||||
|
import WidgetPickerModal from '@frontend/components/widgetDashboard/WidgetPickerModal';
|
||||||
import {
|
import {
|
||||||
defaultGridFr,
|
defaultGridFr,
|
||||||
NATIVE_WIDGET_PICKER_WINDOW,
|
NATIVE_WIDGET_PICKER_WINDOW,
|
||||||
@@ -131,6 +132,8 @@ const WidgetApp: React.FC = () => {
|
|||||||
return () => { unlisten?.(); };
|
return () => { unlisten?.(); };
|
||||||
}, [instance?.id]);
|
}, [instance?.id]);
|
||||||
|
|
||||||
|
const [pickSlotId, setPickSlotId] = useState<string | null>(null);
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
void getCurrentWindow().close();
|
void getCurrentWindow().close();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -174,6 +177,34 @@ const WidgetApp: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleNativePickRequest = useCallback((slotId: string) => {
|
||||||
|
setPickSlotId(slotId);
|
||||||
|
void Promise.resolve(handlePickerOpenChange(true)).catch(err => {
|
||||||
|
console.warn('[widget-main] picker open resize failed', err);
|
||||||
|
});
|
||||||
|
}, [handlePickerOpenChange]);
|
||||||
|
|
||||||
|
const handleNativePickClose = useCallback(() => {
|
||||||
|
setPickSlotId(null);
|
||||||
|
void Promise.resolve(handlePickerOpenChange(false)).catch(err => {
|
||||||
|
console.warn('[widget-main] picker close resize failed', err);
|
||||||
|
});
|
||||||
|
}, [handlePickerOpenChange]);
|
||||||
|
|
||||||
|
const handleNativePickSelect = useCallback((widgetType: string) => {
|
||||||
|
if (!pickSlotId) return;
|
||||||
|
setInstance(prev => {
|
||||||
|
if (!prev) return prev;
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
slots: prev.slots.map(s => (
|
||||||
|
s.id === pickSlotId ? { ...s, widgetType, config: {} } : s
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
handleNativePickClose();
|
||||||
|
}, [pickSlotId, handleNativePickClose]);
|
||||||
|
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
return <p className="wd-widget-empty">위젯 정보를 불러올 수 없습니다.</p>;
|
return <p className="wd-widget-empty">위젯 정보를 불러올 수 없습니다.</p>;
|
||||||
}
|
}
|
||||||
@@ -194,21 +225,32 @@ const WidgetApp: React.FC = () => {
|
|||||||
paperAutoTradeEnabled={false}
|
paperAutoTradeEnabled={false}
|
||||||
chartRealtimeSource="BACKEND_STOMP"
|
chartRealtimeSource="BACKEND_STOMP"
|
||||||
>
|
>
|
||||||
<div className="fw-widget-native-root">
|
<div className={`fw-widget-native-root${pickSlotId ? ' fw-widget-native-root--picker' : ''}`}>
|
||||||
<FloatingWidgetWindow
|
{pickSlotId != null ? (
|
||||||
instance={instance}
|
<WidgetPickerModal
|
||||||
focused
|
open
|
||||||
nativeShell
|
category="all"
|
||||||
onClose={handleClose}
|
nativeCompact
|
||||||
onFocus={() => {}}
|
nativeFullRoot
|
||||||
onPickerOpenChange={handlePickerOpenChange}
|
onClose={handleNativePickClose}
|
||||||
onUpdateSlots={slots => setInstance(prev => (prev ? { ...prev, slots } : prev))}
|
onSelect={handleNativePickSelect}
|
||||||
onUpdateSize={(width, height) => {
|
/>
|
||||||
setInstance(prev => (prev ? { ...prev, width, height } : prev));
|
) : (
|
||||||
void getCurrentWindow().setSize(new LogicalSize(width, height));
|
<FloatingWidgetWindow
|
||||||
}}
|
instance={instance}
|
||||||
onUpdateGridFr={(rowFr, colFr) => setInstance(prev => (prev ? { ...prev, rowFr, colFr } : prev))}
|
focused
|
||||||
/>
|
nativeShell
|
||||||
|
onClose={handleClose}
|
||||||
|
onFocus={() => {}}
|
||||||
|
onNativePickRequest={handleNativePickRequest}
|
||||||
|
onUpdateSlots={slots => setInstance(prev => (prev ? { ...prev, slots } : prev))}
|
||||||
|
onUpdateSize={(width, height) => {
|
||||||
|
setInstance(prev => (prev ? { ...prev, width, height } : prev));
|
||||||
|
void getCurrentWindow().setSize(new LogicalSize(width, height));
|
||||||
|
}}
|
||||||
|
onUpdateGridFr={(rowFr, colFr) => setInstance(prev => (prev ? { ...prev, rowFr, colFr } : prev))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</WidgetDashboardProvider>
|
</WidgetDashboardProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ interface Props {
|
|||||||
onUpdateGridFr: (rowFr: number[], colFr: number[]) => void;
|
onUpdateGridFr: (rowFr: number[], colFr: number[]) => void;
|
||||||
/** Tauri OS 창 — 네이티브 타이틀·리사이즈 사용, 내부 플로팅 크롬 숨김 */
|
/** Tauri OS 창 — 네이티브 타이틀·리사이즈 사용, 내부 플로팅 크롬 숨김 */
|
||||||
nativeShell?: boolean;
|
nativeShell?: boolean;
|
||||||
/** 위젯 선택 팝업 열림/닫힘 (네이티브 창 크기 조절 — 완료 후 picker 표시) */
|
/** 네이티브 위젯 창 — picker는 부모(widget-main)에서 전환 렌더 */
|
||||||
|
onNativePickRequest?: (slotId: string) => void;
|
||||||
|
/** 위젯 선택 팝업 열림/닫힘 (웹 플로팅 레이어용) */
|
||||||
onPickerOpenChange?: (open: boolean) => void | Promise<void>;
|
onPickerOpenChange?: (open: boolean) => void | Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,27 +39,22 @@ const FloatingWidgetWindow: React.FC<Props> = ({
|
|||||||
onUpdateSize,
|
onUpdateSize,
|
||||||
onUpdateGridFr,
|
onUpdateGridFr,
|
||||||
nativeShell = false,
|
nativeShell = false,
|
||||||
|
onNativePickRequest,
|
||||||
onPickerOpenChange,
|
onPickerOpenChange,
|
||||||
}) => {
|
}) => {
|
||||||
const [pickSlotId, setPickSlotId] = useState<string | null>(null);
|
const [pickSlotId, setPickSlotId] = useState<string | null>(null);
|
||||||
|
const nativePickerExternal = nativeShell && Boolean(onNativePickRequest);
|
||||||
|
|
||||||
const openPicker = useCallback((slotId: string) => {
|
const openPicker = useCallback((slotId: string) => {
|
||||||
|
if (nativePickerExternal) {
|
||||||
|
onNativePickRequest!(slotId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setPickSlotId(slotId);
|
setPickSlotId(slotId);
|
||||||
if (nativeShell) {
|
}, [nativePickerExternal, onNativePickRequest]);
|
||||||
document.documentElement.classList.add('fw-native-picker-open');
|
|
||||||
}
|
|
||||||
if (nativeShell && onPickerOpenChange) {
|
|
||||||
void Promise.resolve(onPickerOpenChange(true)).catch(err => {
|
|
||||||
console.warn('[FloatingWidgetWindow] picker window resize failed', err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [nativeShell, onPickerOpenChange]);
|
|
||||||
|
|
||||||
const closePicker = useCallback(async () => {
|
const closePicker = useCallback(async () => {
|
||||||
setPickSlotId(null);
|
setPickSlotId(null);
|
||||||
if (nativeShell) {
|
|
||||||
document.documentElement.classList.remove('fw-native-picker-open');
|
|
||||||
}
|
|
||||||
if (nativeShell && onPickerOpenChange) {
|
if (nativeShell && onPickerOpenChange) {
|
||||||
try {
|
try {
|
||||||
await onPickerOpenChange(false);
|
await onPickerOpenChange(false);
|
||||||
@@ -66,9 +63,6 @@ const FloatingWidgetWindow: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [nativeShell, onPickerOpenChange]);
|
}, [nativeShell, onPickerOpenChange]);
|
||||||
useEffect(() => () => {
|
|
||||||
document.documentElement.classList.remove('fw-native-picker-open');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const bodyWrapRef = useRef<HTMLDivElement>(null);
|
const bodyWrapRef = useRef<HTMLDivElement>(null);
|
||||||
const bodyRef = useRef<HTMLDivElement>(null);
|
const bodyRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -265,7 +259,7 @@ const FloatingWidgetWindow: React.FC<Props> = ({
|
|||||||
<WidgetSlotView
|
<WidgetSlotView
|
||||||
slot={slot}
|
slot={slot}
|
||||||
zoneId="center"
|
zoneId="center"
|
||||||
onPickWidget={id => { void openPicker(id); }}
|
onPickWidget={id => { openPicker(id); }}
|
||||||
onClearWidget={id => clearWidget(id)}
|
onClearWidget={id => clearWidget(id)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -322,7 +316,7 @@ const FloatingWidgetWindow: React.FC<Props> = ({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const pickerModal = pickSlotId != null ? (
|
const pickerModal = !nativePickerExternal && pickSlotId != null ? (
|
||||||
<WidgetPickerModal
|
<WidgetPickerModal
|
||||||
open
|
open
|
||||||
category="all"
|
category="all"
|
||||||
@@ -333,12 +327,7 @@ const FloatingWidgetWindow: React.FC<Props> = ({
|
|||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
if (nativeShell) {
|
if (nativeShell) {
|
||||||
return (
|
return content;
|
||||||
<>
|
|
||||||
{content}
|
|
||||||
{pickerModal && createPortal(pickerModal, document.body)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ interface Props {
|
|||||||
onSelect: (widgetType: string) => void;
|
onSelect: (widgetType: string) => void;
|
||||||
/** Tauri 네이티브 위젯 창 — OS 창 전체를 쓰는 고정형 picker */
|
/** Tauri 네이티브 위젯 창 — OS 창 전체를 쓰는 고정형 picker */
|
||||||
nativeCompact?: boolean;
|
nativeCompact?: boolean;
|
||||||
|
/** 네이티브 위젯 창 루트를 picker 전용으로 쓸 때 (overlay 없음) */
|
||||||
|
nativeFullRoot?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function pickerMeta(category: WidgetPickerCategory) {
|
function pickerMeta(category: WidgetPickerCategory) {
|
||||||
@@ -69,36 +71,45 @@ const NativeWidgetPicker: React.FC<Props> = ({
|
|||||||
category,
|
category,
|
||||||
onClose,
|
onClose,
|
||||||
onSelect,
|
onSelect,
|
||||||
|
nativeFullRoot = false,
|
||||||
}) => {
|
}) => {
|
||||||
if (!open) return null;
|
if (!open) return null;
|
||||||
const { title } = pickerMeta(category);
|
const { title } = pickerMeta(category);
|
||||||
|
|
||||||
|
const shell = (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (nativeFullRoot) {
|
||||||
|
return <div className="wd-picker-native-root">{shell}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="wd-picker-native-overlay" role="presentation">
|
<div className="wd-picker-native-overlay" role="presentation">
|
||||||
<div
|
{shell}
|
||||||
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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -109,6 +120,7 @@ const WidgetPickerModal: React.FC<Props> = ({
|
|||||||
onClose,
|
onClose,
|
||||||
onSelect,
|
onSelect,
|
||||||
nativeCompact = false,
|
nativeCompact = false,
|
||||||
|
nativeFullRoot = false,
|
||||||
}) => {
|
}) => {
|
||||||
if (!open) return null;
|
if (!open) return null;
|
||||||
|
|
||||||
@@ -119,6 +131,7 @@ const WidgetPickerModal: React.FC<Props> = ({
|
|||||||
category={category}
|
category={category}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
|
nativeFullRoot={nativeFullRoot}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1317,6 +1317,29 @@ html.fw-native-widget .wd-picker-native-body {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 네이티브 위젯 창 — picker가 창 전체를 차지 (overlay/portal 미사용) */
|
||||||
|
html.fw-native-widget .fw-widget-native-root--picker {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.fw-native-widget .wd-picker-native-root {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--bg, #1a1b26);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.fw-native-widget .wd-picker-native-root .wd-picker-native-shell {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
html.fw-native-widget .wd-picker-native-loading {
|
html.fw-native-widget .wd-picker-native-loading {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@@ -1329,16 +1352,6 @@ html.fw-native-widget .wd-picker-native-loading {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.fw-native-widget.fw-native-picker-open .fw-widget-native-root .fw-window {
|
|
||||||
visibility: hidden;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.fw-native-widget.fw-native-picker-open .wd-picker-native-overlay {
|
|
||||||
padding: 0;
|
|
||||||
background: var(--bg, #1a1b26);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bps-page--wd-no-left .bps-side-wrap--left,
|
.bps-page--wd-no-left .bps-side-wrap--left,
|
||||||
.bps-page--wd-no-left .bps-side-wrap--left + .bps-splitter {
|
.bps-page--wd-no-left .bps-side-wrap--left + .bps-splitter {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
|
|||||||
Reference in New Issue
Block a user