From d19dabdc3d79c614fcab8c59a7a883847afc8d54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=97=B0=EC=9A=B0?= Date: Sat, 25 Jul 2026 19:17:43 +0900 Subject: [PATCH 1/7] =?UTF-8?q?fix:=20=EB=B6=84=EB=A6=AC=20=ED=8C=A8?= =?UTF-8?q?=EB=84=90=20=ED=94=BC=EC=BB=A4=EB=A5=BC=20=EC=84=B9=EC=85=98=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC=EB=A1=9C=20=EB=B0=B0=EC=B9=98=ED=95=98?= =?UTF-8?q?=EA=B3=A0=20=EC=B0=BD=20=EC=B5=9C=EC=86=8C=20=EB=86=92=EC=9D=B4?= =?UTF-8?q?=20=EC=83=81=ED=96=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/state/app_state.rs | 92 +++-- .../activeStatePickerCapability.test.tsx | 1 + .../floatingPopupLayerOwnership.test.tsx | 130 +++++++ .../panelAnchoredPopupPosition.test.ts | 20 +- .../__tests__/pickerSurfacePlacement.test.tsx | 199 ++++++++++ .../Grid/PropertiesPanel/PickerSurface.tsx | 124 ++++++ .../Grid/PropertiesPanel/PropertyInputs.tsx | 9 +- .../components/main/Modal/FloatingPopup.tsx | 14 +- .../Modal/content/pickers/ColorPicker.tsx | 353 ++++++++---------- .../Modal/content/pickers/ImagePicker.tsx | 244 ++++++------ .../Modal/content/pickers/ShadowPicker.tsx | 210 +++++------ .../components/main/Modal/popupLayer.ts | 15 + .../hooks/ui/usePanelAnchoredPopupPosition.ts | 133 +++++++ 13 files changed, 1072 insertions(+), 472 deletions(-) create mode 100644 src/renderer/__tests__/floatingPopupLayerOwnership.test.tsx create mode 100644 src/renderer/__tests__/pickerSurfacePlacement.test.tsx create mode 100644 src/renderer/components/main/Grid/PropertiesPanel/PickerSurface.tsx diff --git a/src-tauri/src/state/app_state.rs b/src-tauri/src/state/app_state.rs index 3569a074..710096d9 100644 --- a/src-tauri/src/state/app_state.rs +++ b/src-tauri/src/state/app_state.rs @@ -63,8 +63,10 @@ const TRAY_MENU_QUIT_ID: &str = "tray-quit"; const DEFAULT_OVERLAY_WIDTH: f64 = 860.0; const DEFAULT_OVERLAY_HEIGHT: f64 = 320.0; const PANEL_WIDTH: f64 = 240.0; -const PANEL_INITIAL_HEIGHT: f64 = 530.0; -const PANEL_MIN_HEIGHT: f64 = 360.0; +// 피커가 트리거 행 아래에 그대로 들어갈 세로 여유 - 이보다 낮으면 팝업이 +// 매번 위로 뒤집히거나 창 경계로 클램프됨. 늘리는 것만 허용 +const PANEL_INITIAL_HEIGHT: f64 = 712.0; +const PANEL_MIN_HEIGHT: f64 = 712.0; const PANEL_MAX_HEIGHT_RATIO: f64 = 0.9; const PANEL_FALLBACK_MAX_HEIGHT: f64 = 10_000.0; const PANEL_BOUNDS_DEBOUNCE_MS: u64 = 400; @@ -2896,7 +2898,7 @@ impl AppState { .accept_first_mouse(true) .visible(true) .inner_size(PANEL_WIDTH, layout.height) - .min_inner_size(PANEL_WIDTH, PANEL_MIN_HEIGHT) + .min_inner_size(PANEL_WIDTH, layout.min_height) .max_inner_size(PANEL_WIDTH, layout.max_height) .zoom_hotkeys_enabled(false); @@ -4416,14 +4418,20 @@ impl MonitorData { struct PanelWindowLayout { position: Option, height: f64, + min_height: f64, max_height: f64, } -fn panel_max_height(work_area_height: Option) -> f64 { - work_area_height - .filter(|height| height.is_finite() && *height > 0.0) - .map(|height| (height * PANEL_MAX_HEIGHT_RATIO).max(PANEL_MIN_HEIGHT)) - .unwrap_or(PANEL_FALLBACK_MAX_HEIGHT) +// 작업 영역이 하한보다 좁으면 하한을 화면에 맞춰 낮춤 - 그러지 않으면 창 아래쪽이 +// 화면 밖으로 나가 리사이즈 가장자리에 손이 닿지 않는다 +fn panel_height_bounds(work_area_height: Option) -> (f64, f64) { + let Some(work_area_height) = + work_area_height.filter(|height| height.is_finite() && *height > 0.0) + else { + return (PANEL_MIN_HEIGHT, PANEL_FALLBACK_MAX_HEIGHT); + }; + let max_height = work_area_height * PANEL_MAX_HEIGHT_RATIO; + (PANEL_MIN_HEIGHT.min(max_height), max_height) } fn resolve_panel_window_layout( @@ -4436,13 +4444,14 @@ fn resolve_panel_window_layout( monitors.find_best_overlap(bounds.x, bounds.y, PANEL_WIDTH, bounds.height) }) .or_else(|| monitors.primary_spec()); - let max_height = panel_max_height(target_monitor.map(|monitor| monitor.logical_height)); + let (min_height, max_height) = + panel_height_bounds(target_monitor.map(|monitor| monitor.logical_height)); // 저장된 높이가 없으면 메인 창 높이를 기본값으로 (프로그램 높이 동기) let requested_height = stored_bounds .map(|bounds| bounds.height) .or(fallback_height) .unwrap_or(PANEL_INITIAL_HEIGHT); - let height = requested_height.clamp(PANEL_MIN_HEIGHT, max_height); + let height = requested_height.clamp(min_height, max_height); let position = stored_bounds.map(|bounds| { target_monitor .map(|monitor| monitor.clamp(bounds.x, bounds.y, PANEL_WIDTH, height)) @@ -4455,6 +4464,7 @@ fn resolve_panel_window_layout( PanelWindowLayout { position, height, + min_height, max_height, } } @@ -4498,7 +4508,7 @@ fn panel_bounds_from_sample(sample: PanelBoundsSample) -> PanelBounds { PanelBounds { x: position.x, y: position.y, - height: size.height.max(PANEL_MIN_HEIGHT), + height: size.height, } } @@ -4698,10 +4708,10 @@ impl PanelBoundsPersistenceController { session: u64, generation: u64, ) -> Result<()> { - let Some(max_height) = window + let Some((min_height, monitor_max_height)) = window .current_monitor()? .and_then(MonitorSpec::from_monitor) - .map(|monitor| panel_max_height(Some(monitor.logical_height))) + .map(|monitor| panel_height_bounds(Some(monitor.logical_height))) else { return Ok(()); }; @@ -4710,11 +4720,16 @@ impl PanelBoundsPersistenceController { if !state.active || state.session != session || state.generation != generation { return Ok(()); } - changed_panel_max_height(state.applied_max_height, max_height) + changed_panel_max_height(state.applied_max_height, monitor_max_height) }; let Some(max_height) = max_height else { return Ok(()); }; + // 좁은 모니터로 옮겨가면 하한도 함께 내려야 창이 화면 밖으로 나가지 않음 + window.set_min_size(Some(tauri::Size::Logical(tauri::LogicalSize::new( + PANEL_WIDTH, + min_height, + ))))?; window.set_max_size(Some(tauri::Size::Logical(tauri::LogicalSize::new( PANEL_WIDTH, max_height, @@ -4905,15 +4920,15 @@ mod tests { changed_panel_max_height, collect_authorized_css_paths, collect_frontend_lifecycle_targets, frontend_history_mutation_blocked, frontend_lifecycle_restore_labels, global_css_watch_path, install_history_handshake, install_lifecycle_handshake, - key_state_payload, next_keyboard_recovery_plan, panel_bounds_from_sample, panel_max_height, - publish_panel_hidden_transition, publish_panel_visibility_transition, + key_state_payload, next_keyboard_recovery_plan, panel_bounds_from_sample, + panel_height_bounds, publish_panel_hidden_transition, publish_panel_visibility_transition, publish_selection_snapshot, resolve_event_age_ms, resolve_panel_window_layout, run_panel_close_timeout, should_create_overlay_on_startup, should_recover_keyboard_daemon, take_cancelable_editor_flush_handshake, take_editor_flush_handshake, take_targeted_panel_view_state, validate_selection_session, EditorFlushAcknowledge, EditorFlushCompletion, EditorFlushHandshake, EditorFlushRequest, FrontendFlushAction, FrontendHistoryFlushPhase, FrontendHistoryFlushReady, FrontendLifecycleAction, - LifecycleHandshakeInstall, MonitorData, Mutex, PanelBoundsChange, + LifecycleHandshakeInstall, MonitorData, MonitorSpec, Mutex, PanelBoundsChange, PanelBoundsPersistenceController, PanelBoundsPersistenceState, PanelBoundsSample, PanelCloseRequestState, PanelCloseRequestedPayload, PanelLayerTab, PanelPropertyTab, PanelViewMode, PanelViewState, PanelViewTarget, PanelVisibilityEventEmitter, @@ -4945,9 +4960,41 @@ mod tests { assert_eq!(PANEL_LABEL, "panel"); assert_eq!(PANEL_ENTRYPOINT, "panel/index.html"); assert_eq!(PANEL_WIDTH, 240.0); - assert_eq!(PANEL_INITIAL_HEIGHT, 530.0); - assert_eq!(PANEL_MIN_HEIGHT, 360.0); - assert_eq!(panel_max_height(Some(1_000.0)), 900.0); + assert_eq!(PANEL_INITIAL_HEIGHT, 712.0); + assert_eq!(PANEL_MIN_HEIGHT, 712.0); + // 넉넉한 화면에서는 하한이 그대로 유지됨 + assert_eq!( + panel_height_bounds(Some(1_000.0)), + (PANEL_MIN_HEIGHT, 900.0) + ); + // 하한보다 좁은 작업 영역에서는 하한이 화면에 맞춰 내려감 - clamp 역전 방지 + assert_eq!(panel_height_bounds(Some(600.0)), (540.0, 540.0)); + let (min_height, max_height) = panel_height_bounds(None); + assert_eq!(min_height, PANEL_MIN_HEIGHT); + assert!(max_height >= min_height); + } + + #[test] + fn panel_layout_never_exceeds_a_small_work_area() { + let monitors = MonitorData { + specs: vec![MonitorSpec { + logical_origin_x: 0.0, + logical_origin_y: 0.0, + logical_width: 1_280.0, + logical_height: 680.0, + physical_origin_x: 0.0, + physical_origin_y: 0.0, + physical_width: 1_280.0, + physical_height: 680.0, + scale_factor: 1.0, + }], + primary_index: Some(0), + }; + let layout = resolve_panel_window_layout(None, &monitors, None); + + assert!(layout.min_height <= layout.max_height); + assert!(layout.height <= 680.0); + assert_eq!(layout.height, layout.max_height); } #[test] @@ -5009,14 +5056,15 @@ mod tests { let bounds = panel_bounds_from_sample(PanelBoundsSample { position: PhysicalPosition::new(600, 300), position_scale_factor: 2.0, - size: PhysicalSize::new(480, 1_000), + size: PhysicalSize::new(480, 1_600), size_scale_factor: 2.0, current_scale_factor: 2.0, }); assert_eq!(bounds.x, 300.0); assert_eq!(bounds.y, 150.0); - assert_eq!(bounds.height, 500.0); + // 표본은 실측 그대로 - 하한은 복원 시 모니터 기준으로 다시 적용됨 + assert_eq!(bounds.height, 800.0); } #[test] diff --git a/src/renderer/__tests__/activeStatePickerCapability.test.tsx b/src/renderer/__tests__/activeStatePickerCapability.test.tsx index b1f1273e..796dd979 100644 --- a/src/renderer/__tests__/activeStatePickerCapability.test.tsx +++ b/src/renderer/__tests__/activeStatePickerCapability.test.tsx @@ -30,6 +30,7 @@ vi.mock('@components/main/Modal/FloatingPopup', () => ({ vi.mock('@hooks/ui/usePanelAnchoredPopupPosition', () => ({ usePanelAnchoredPopupPosition: () => null, + useTriggerAnchoredPopupPosition: () => ({ settled: true, position: null }), })); vi.mock('@contexts/useTranslation', () => ({ diff --git a/src/renderer/__tests__/floatingPopupLayerOwnership.test.tsx b/src/renderer/__tests__/floatingPopupLayerOwnership.test.tsx new file mode 100644 index 00000000..bb2383c3 --- /dev/null +++ b/src/renderer/__tests__/floatingPopupLayerOwnership.test.tsx @@ -0,0 +1,130 @@ +// @vitest-environment jsdom +import React, { act, useRef } from 'react'; +import { createRoot, type Root } from 'react-dom/client'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import FloatingPopup from '@components/main/Modal/FloatingPopup'; + +// floating-ui autoUpdate가 요구 — 배치는 fixed 좌표라 관찰 결과가 필요 없음 +class ResizeObserverStub { + observe() {} + unobserve() {} + disconnect() {} +} + +globalThis.ResizeObserver ??= + ResizeObserverStub as unknown as typeof ResizeObserver; + +// 부모 팝업 안의 트리거로 자식 팝업을 여는 실제 구조 (그림자 → 색상) +const Nested = ({ + childOpen, + onParentClose, + onChildClose, +}: { + childOpen: boolean; + onParentClose: () => void; + onChildClose: () => void; +}) => { + const parentTriggerRef = useRef(null); + const childTriggerRef = useRef(null); + + return ( + <> + + )} + - {/* 초기화 칩 — 이미지가 있을 때만, 프리뷰 호버 시 표시 */} - {currentImage && ( - - )} + {/* 설정 카드 */} + + {/* 키 투명화 토글 */} +
+

+ {t('imagePicker.transparent')} +

+
- {/* 설정 카드 */} - - {/* 키 투명화 토글 */} + {/* 이미지 맞춤 */} + {showImageFit && (

- {t('imagePicker.transparent')} + {t('propertiesPanel.imageFit') || '표시'}

-
- - {/* 이미지 맞춤 */} - {showImageFit && ( -
-

- {t('propertiesPanel.imageFit') || '표시'} -

- -
- )} -
- - + )} +
+ ); }; diff --git a/src/renderer/components/main/Modal/content/pickers/ShadowPicker.tsx b/src/renderer/components/main/Modal/content/pickers/ShadowPicker.tsx index 80942b48..75f01c0c 100644 --- a/src/renderer/components/main/Modal/content/pickers/ShadowPicker.tsx +++ b/src/renderer/components/main/Modal/content/pickers/ShadowPicker.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef, useState } from 'react'; import type { ElementShadowSpec } from '@src/types/key/shadows'; -import FloatingPopup from '../../FloatingPopup'; +import PickerSurface from '@components/main/Grid/PropertiesPanel/PickerSurface'; import ColorPicker from './ColorPicker'; import { ColorSwatchButton } from './ColorSwatch'; import TabSwitch from '@components/main/common/TabSwitch'; @@ -9,7 +9,6 @@ import { PropertyRow, PropertySection, } from '@components/main/Grid/PropertiesPanel/PropertyInputs'; -import { usePanelAnchoredPopupPosition } from '@hooks/ui/usePanelAnchoredPopupPosition'; type ShadowState = 'idle' | 'active'; @@ -88,129 +87,108 @@ const ShadowPicker = ({ onClose(); }; - const pickerContainerRef = useRef(null); - const fixedPosition = usePanelAnchoredPopupPosition({ - open, - panelElement, - referenceRef, - popupRef: pickerContainerRef, - fallbackWidth: 204, - fallbackHeight: 210, - }); - - const effectiveOffsetY = fixedPosition ? 0 : -93; + const shadowLabel = t('propertiesPanel.shadow') || '그림자'; return ( - -
- {/* 상태 전환 — 눌림 상태가 없는 요소는 대기만 */} - {showActiveState ? ( - { + if (typeof color === 'string') setDraftColor(color); + }} + onColorChangeComplete={(color) => { + if (typeof color !== 'string') return; + setDraftColor(color); + update({ color }); + }} + onClose={() => setColorOpen(false)} + solidOnly + placement="left-start" + offsetY={0} + interactiveRefs={[colorButtonRef]} /> - ) : null} - - - - {mixed ? ( - Mixed - ) : null} - setColorOpen((prev) => !prev)} - open={colorOpen} - className="w-[23px] h-[23px] rounded-md cursor-pointer transition-shadow flex-shrink-0" - surfaceClassName="rounded-md" - color={draftColor} - /> - - - - update({ offsetX: value })} - prefix="X" - min={-100} - max={100} - allowDecimal - decimalScale={1} - /> - update({ offsetY: value })} - prefix="Y" - min={-100} - max={100} - allowDecimal - decimalScale={1} - /> - - - - update({ blur: value })} - suffix="px" - min={0} - max={100} - allowDecimal - decimalScale={1} - /> - - -
- - {colorOpen ? ( - { - if (typeof color === 'string') setDraftColor(color); - }} - onColorChangeComplete={(color) => { - if (typeof color !== 'string') return; - setDraftColor(color); - update({ color }); - }} - onClose={() => setColorOpen(false)} - solidOnly - placement="left-start" - offsetY={0} - interactiveRefs={[colorButtonRef]} + ) : null + } + > + {/* 상태 전환 — 눌림 상태가 없는 요소는 대기만 */} + {showActiveState ? ( + ) : null} -
+ + + + {mixed ? ( + Mixed + ) : null} + setColorOpen((prev) => !prev)} + open={colorOpen} + className="w-[23px] h-[23px] rounded-md cursor-pointer transition-shadow flex-shrink-0" + surfaceClassName="rounded-md" + color={draftColor} + /> + + + + update({ offsetX: value })} + prefix="X" + min={-100} + max={100} + allowDecimal + decimalScale={1} + /> + update({ offsetY: value })} + prefix="Y" + min={-100} + max={100} + allowDecimal + decimalScale={1} + /> + + + + update({ blur: value })} + suffix="px" + min={0} + max={100} + allowDecimal + decimalScale={1} + /> + + + ); }; diff --git a/src/renderer/components/main/Modal/popupLayer.ts b/src/renderer/components/main/Modal/popupLayer.ts index 85fcdb0a..3cb14d8f 100644 --- a/src/renderer/components/main/Modal/popupLayer.ts +++ b/src/renderer/components/main/Modal/popupLayer.ts @@ -24,3 +24,18 @@ export const isTopmostPopupLayer = (element: HTMLElement | null) => { removeDisconnectedLayers(); return popupLayerStack[popupLayerStack.length - 1] === element; }; + +// 위에 쌓인 레이어 안쪽을 가리키는지 — 자식 팝업이 body로 포털돼 부모 DOM 밖에 있어도 +// 그 클릭으로 부모가 닫히면 안 된다 (Escape 소유권과 같은 규칙) +export const isInsideHigherPopupLayer = ( + element: HTMLElement | null, + target: Node | null, +) => { + if (!element || !target) return false; + removeDisconnectedLayers(); + const index = popupLayerStack.lastIndexOf(element); + if (index < 0) return false; + return popupLayerStack + .slice(index + 1) + .some((layer) => layer.contains(target)); +}; diff --git a/src/renderer/hooks/ui/usePanelAnchoredPopupPosition.ts b/src/renderer/hooks/ui/usePanelAnchoredPopupPosition.ts index d83399cb..028b832e 100644 --- a/src/renderer/hooks/ui/usePanelAnchoredPopupPosition.ts +++ b/src/renderer/hooks/ui/usePanelAnchoredPopupPosition.ts @@ -61,6 +61,56 @@ export const getPanelAnchoredPopupPosition = ({ }; }; +interface TriggerAnchoredPositionOptions { + /** 트리거가 속한 속성 섹션 카드 — 팝업의 좌우 정렬·폭 기준 */ + sectionRect: { left: number; width: number }; + /** 팝업을 연 행 */ + triggerRect: { top: number; bottom: number }; + popupHeight: number; + viewportWidth: number; + viewportHeight: number; + gap?: number; + padding?: number; +} + +export interface TriggerAnchoredPosition extends PopupPosition { + width: number; +} + +export interface TriggerAnchoredResult { + /** 앵커 탐색이 끝났는지 — 끝나기 전에는 호출자가 팝업을 감춰 첫 프레임 튐을 막는다 */ + settled: boolean; + /** 섹션 앵커가 없으면 null — 호출자가 기본 배치로 폴백 */ + position: TriggerAnchoredPosition | null; +} + +// 폭과 좌우 정렬은 섹션 카드에 맞추고, 세로는 트리거 행 바로 아래. +// 아래 공간이 모자라면 행 위로 뒤집고, 그래도 안 들어가면 화면 안으로 클램프 +export const getTriggerAnchoredPopupPosition = ({ + sectionRect, + triggerRect, + popupHeight, + viewportWidth, + viewportHeight, + gap = 5, + padding = 5, +}: TriggerAnchoredPositionOptions): TriggerAnchoredPosition => { + const width = Math.min(sectionRect.width, viewportWidth - padding * 2); + const maxX = Math.max(padding, viewportWidth - width - padding); + const x = Math.min(Math.max(sectionRect.left, padding), maxX); + + const below = triggerRect.bottom + gap; + const above = triggerRect.top - gap - popupHeight; + const maxY = viewportHeight - popupHeight - padding; + const y = below <= maxY || above < padding ? below : above; + + return { + x, + y: Math.min(Math.max(y, padding), Math.max(padding, maxY)), + width, + }; +}; + interface UsePanelAnchoredPopupPositionOptions { open: boolean; panelElement?: HTMLElement | null; @@ -145,3 +195,86 @@ export const usePanelAnchoredPopupPosition = ({ return position; }; + +const UNSETTLED: TriggerAnchoredResult = { settled: false, position: null }; +const NO_ANCHOR: TriggerAnchoredResult = { settled: true, position: null }; + +interface UseTriggerAnchoredPopupPositionOptions { + open: boolean; + referenceRef?: RefObject; + popupRef: RefObject; + fallbackHeight: number; +} + +export const useTriggerAnchoredPopupPosition = ({ + open, + referenceRef, + popupRef, + fallbackHeight, +}: UseTriggerAnchoredPopupPositionOptions): TriggerAnchoredResult => { + const [result, setResult] = useState(UNSETTLED); + const computePositionRef = useRef<(() => void) | null>(null); + + useLayoutEffect(() => { + const trigger = referenceRef?.current; + const section = trigger?.closest('[data-dmn-section="true"]'); + if (!open) { + setResult(UNSETTLED); + computePositionRef.current = null; + return; + } + // 섹션 밖 트리거는 정렬 기준이 없음 — 감추지 말고 기본 배치로 넘김 + if (!trigger || !section) { + setResult(NO_ANCHOR); + computePositionRef.current = null; + return; + } + + // 앵커는 열리는 시점에 한 번만 캡처 — 패널 스크롤·리렌더에도 제자리 유지 + const triggerRect = trigger.getBoundingClientRect(); + const sectionRect = section.getBoundingClientRect(); + + const compute = () => { + const popupElement = popupRef.current; + const next = getTriggerAnchoredPopupPosition({ + sectionRect, + triggerRect, + popupHeight: popupElement?.offsetHeight || fallbackHeight, + viewportWidth: window.innerWidth, + viewportHeight: window.innerHeight, + }); + + setResult((previous) => { + const current = previous.position; + return current && + current.x === next.x && + current.y === next.y && + current.width === next.width + ? previous + : { settled: true, position: next }; + }); + }; + + computePositionRef.current = compute; + compute(); + + const observer = + typeof ResizeObserver === 'undefined' + ? null + : new ResizeObserver(() => compute()); + if (popupRef.current) observer?.observe(popupRef.current); + window.addEventListener('resize', compute); + + return () => { + observer?.disconnect(); + window.removeEventListener('resize', compute); + computePositionRef.current = null; + }; + }, [fallbackHeight, open, popupRef, referenceRef]); + + useLayoutEffect(() => { + computePositionRef.current?.(); + }); + + return result; +}; From 620c6892cb4574f6c466a6c7df99edfa704c3732 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=97=B0=EC=9A=B0?= Date: Sun, 26 Jul 2026 01:17:05 +0900 Subject: [PATCH 2/7] =?UTF-8?q?refactor:=20=EB=8B=A8=EC=B6=95=ED=82=A4=20?= =?UTF-8?q?=ED=8C=A8=EB=84=90=20=ED=95=98=EB=8B=A8=20=EC=95=88=EB=82=B4=20?= =?UTF-8?q?=EB=AC=B8=EA=B5=AC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/main/SettingsPanel/ShortcutsPanelContent.tsx | 5 ----- src/renderer/locales/en.json | 1 - src/renderer/locales/ko.json | 1 - src/renderer/locales/ru.json | 1 - src/renderer/locales/zh-Hant.json | 1 - src/renderer/locales/zh-cn.json | 1 - 6 files changed, 10 deletions(-) diff --git a/src/renderer/components/main/SettingsPanel/ShortcutsPanelContent.tsx b/src/renderer/components/main/SettingsPanel/ShortcutsPanelContent.tsx index 6e7745ba..ed481175 100644 --- a/src/renderer/components/main/SettingsPanel/ShortcutsPanelContent.tsx +++ b/src/renderer/components/main/SettingsPanel/ShortcutsPanelContent.tsx @@ -335,11 +335,6 @@ const ShortcutsPanelContent = ({ {notice} ) : null} - - {/* 우클릭 해제·Backspace 초기화 안내 - 구 모달의 발견성 복원 */} -

- {t('shortcutSetting.hint')} -

diff --git a/src/renderer/locales/en.json b/src/renderer/locales/en.json index 1aaefd5b..9fc90e73 100644 --- a/src/renderer/locales/en.json +++ b/src/renderer/locales/en.json @@ -121,7 +121,6 @@ "zoomOut": "Zoom Out", "resetZoom": "Reset Zoom", "listening": "Press keys...", - "hint": "Right-click to unbind. While listening: Backspace to clear, Esc to cancel.", "unassigned": "Unassigned", "movedFrom": "This shortcut was unassigned from {{name}}.", "saveFailed": "Failed to save shortcuts." diff --git a/src/renderer/locales/ko.json b/src/renderer/locales/ko.json index 0d079162..70ee3c4a 100644 --- a/src/renderer/locales/ko.json +++ b/src/renderer/locales/ko.json @@ -121,7 +121,6 @@ "zoomOut": "줌 축소", "resetZoom": "줌 초기화", "listening": "키 입력...", - "hint": "우클릭: 해제, (입력 대기 중) Backspace: 초기화, Esc: 취소", "unassigned": "미지정", "movedFrom": "{{name}}의 기존 단축키가 해제되었습니다.", "saveFailed": "단축키 저장에 실패했습니다." diff --git a/src/renderer/locales/ru.json b/src/renderer/locales/ru.json index 430ab08a..5d073287 100644 --- a/src/renderer/locales/ru.json +++ b/src/renderer/locales/ru.json @@ -121,7 +121,6 @@ "zoomOut": "Отдалить", "resetZoom": "Сброс масштаба", "listening": "Нажмите клавиши...", - "hint": "ПКМ снять. При вводе: Backspace — очистить, Esc — отмена.", "unassigned": "Не назначено", "movedFrom": "Это сочетание снято с действия «{{name}}».", "saveFailed": "Ошибка сохранения." diff --git a/src/renderer/locales/zh-Hant.json b/src/renderer/locales/zh-Hant.json index f9b04897..71ea74e9 100644 --- a/src/renderer/locales/zh-Hant.json +++ b/src/renderer/locales/zh-Hant.json @@ -121,7 +121,6 @@ "zoomOut": "縮小", "resetZoom": "重置縮放", "listening": "按下按鍵...", - "hint": "右鍵單擊解除綁定. 監聽時: 退格鍵清除, Esc 取消.", "unassigned": "未分配", "movedFrom": "已從「{{name}}」移除該快捷鍵。", "saveFailed": "儲存快捷鍵失敗." diff --git a/src/renderer/locales/zh-cn.json b/src/renderer/locales/zh-cn.json index df8eeefd..88cdfca9 100644 --- a/src/renderer/locales/zh-cn.json +++ b/src/renderer/locales/zh-cn.json @@ -121,7 +121,6 @@ "zoomOut": "缩小", "resetZoom": "重置缩放", "listening": "按下按键...", - "hint": "右键单击解除绑定. 监听时: 退格键清除, Esc 取消.", "unassigned": "未分配", "movedFrom": "已从“{{name}}”移除该快捷键。", "saveFailed": "保存快捷键失败." From 74fe4209077c3568086142c6e24589ebcf0a1909 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=97=B0=EC=9A=B0?= Date: Sun, 26 Jul 2026 01:17:05 +0900 Subject: [PATCH 3/7] =?UTF-8?q?feat:=20=EC=84=A4=EC=A0=95=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=ED=96=89=20=ED=81=B4=EB=A6=AD=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=ED=95=AD=EB=AA=A9=20=EB=A9=94=EB=89=B4=20=EC=97=B4=EA=B3=A0=20?= =?UTF-8?q?=ED=98=B8=EB=B2=84=20=EB=B0=9D=EA=B8=B0=EB=A1=9C=20=EC=8B=A0?= =?UTF-8?q?=ED=98=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/SettingsPanel/CssPanelContent.tsx | 21 ++++++++++-- .../SettingsPanel/PluginsPanelContent.tsx | 16 ++++++++- .../main/SettingsPanel/panelChrome.ts | 10 +++++- src/renderer/hooks/usePickerItemMenu.ts | 34 +++++++++++++++---- 4 files changed, 71 insertions(+), 10 deletions(-) diff --git a/src/renderer/components/main/SettingsPanel/CssPanelContent.tsx b/src/renderer/components/main/SettingsPanel/CssPanelContent.tsx index a622cba1..7157682e 100644 --- a/src/renderer/components/main/SettingsPanel/CssPanelContent.tsx +++ b/src/renderer/components/main/SettingsPanel/CssPanelContent.tsx @@ -12,7 +12,9 @@ import { PANEL_LIST_SCROLL_CLASS, PANEL_LIST_WELL_CLASS, PANEL_PILL_CLASS, + PANEL_ROW_NAME_ACTIVE_CLASS, PANEL_ROW_NAME_CLASS, + PANEL_ROW_NAME_UNAVAILABLE_CLASS, PANEL_SECTION_CLASS, PANEL_STATUS_BADGE_CLASS, } from '@components/main/SettingsPanel/panelChrome'; @@ -207,6 +209,16 @@ const CssPanelContent = ({ return (
menu.capturePressState(item.path)} + onClick={(event) => menu.openFromRow(event, item.path)} + onKeyDown={(event) => { + if (event.target !== event.currentTarget) return; + if (event.key === 'Enter' || event.key === ' ') { + menu.openFromRow(event, item.path); + } + }} onContextMenu={(event) => menu.openFromContextMenu(event, item.path) } @@ -215,7 +227,9 @@ const CssPanelContent = ({ > {pathBaseName(item.path)} @@ -229,7 +243,10 @@ const CssPanelContent = ({ ) : (