앱 위젯추가 팝업 수정
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user