From 7184e1ede0a3dfff74ddfae594df9afb47a83bac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=97=B0=EC=9A=B0?= Date: Sat, 11 Jul 2026 19:18:59 +0900 Subject: [PATCH 01/17] =?UTF-8?q?fix:=20=ED=8C=A8=EB=84=90=C2=B7=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=EC=83=81=ED=98=B8=EC=9E=91=EC=9A=A9=20=EA=B2=B0?= =?UTF-8?q?=ED=95=A8=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EB=8D=B0=EB=93=9C?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EB=8C=80=EC=B2=AD=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 패널·입력 상호작용: - 패널 본문 종류 전환 시 서브 페이지가 남아 패널이 비는 회귀 수정 (panelScopeKey 리셋) - Escape 레이어 소유권 확립: 메뉴 → 페이지 → 그리드 선택 해제 순으로 한 겹씩만 소비 (캡처 단계 + preventDefault, 그리드는 양보) - 슬라이드 아웃된 패널 페이지에 inert — 키보드 포커스·접근성 트리 제외 - TabList 인터랙티브 중첩 해소, 삭제 버튼 키보드 노출 + aria-label 모달 등장 모션 재구성: - 배경 블러를 형제 스크림 언더레이로 이동 (조상 backdrop root가 모달 글래스 블러를 무력화하던 구조 해소, 배경 블러는 유지) - 래퍼 opacity 애니메이션 제거 — 페이드 중 블러가 죽었다 튀던 원인. 스크림은 modalScrim(틴트+블러 동시 보간), 콘텐츠는 modalScale이 소유 대청소 (-1157줄): - 미사용 프리미티브 삭제: Button·IconButton·Input·Radio·SelectInput - 죽은 스크롤바 체계 일괄 제거: 전역 숨김에 무력화된 ::-webkit 블록, show-scrollbar 토글, 패널 thumb 계산 머신, useLenis 스크롤바 실측 - CommonListPickerPopup no-op prop 3종 제거, Dropdown size 변형 통합 토큰 확장: 라운딩 3단(--ui-radius-inner/surface/modal) 신설 및 전면 스윕, danger muted 인터랙션 래더, GridMinimap 리터럴 토큰화 --- .../components/main/Grid/PropertiesPanel.tsx | 105 +++----- .../PluginSettingsPanelView.tsx | 9 - .../Grid/PropertiesPanel/PropertyInputs.tsx | 127 +--------- .../batch/BatchSelectionPanel.tsx | 41 ---- .../main/Grid/PropertiesPanel/index.ts | 1 - .../Grid/PropertiesPanel/layer/LayerPanel.tsx | 2 +- .../PropertiesPanel/layer/LayerTabContent.tsx | 50 +--- .../single/SingleSelectionPanel.tsx | 50 ---- .../main/Grid/PropertiesPanel/types.ts | 6 - .../Grid/PropertiesPanel/usePanelScroll.ts | 178 +------------- .../components/main/Grid/core/GridMinimap.tsx | 2 +- .../components/main/Modal/FloatingPopup.tsx | 6 +- .../components/main/Modal/ListPopup.tsx | 8 +- .../main/Modal/ManagerModalLayout.tsx | 2 +- src/renderer/components/main/Modal/Modal.tsx | 18 +- .../main/Modal/content/dialogs/Alert.tsx | 26 +- .../main/Modal/content/dialogs/Laboratory.tsx | 2 +- .../content/dialogs/PluginDataDeleteModal.tsx | 5 +- .../content/dialogs/UnifiedKeySetting.tsx | 15 +- .../Modal/content/dialogs/UpdateModal.tsx | 2 +- .../editors/CounterAnimationEditorModal.tsx | 8 +- .../Modal/content/editors/TabCssModal.tsx | 4 +- .../Modal/content/editors/TabNameModal.tsx | 2 +- .../Modal/content/managers/SoundTrimModal.tsx | 2 +- .../Modal/content/pickers/ColorPicker.tsx | 2 +- .../content/pickers/CommonListPickerPopup.tsx | 29 +-- .../pickers/CounterAnimationPicker.tsx | 2 - .../main/Modal/content/pickers/FontPicker.tsx | 4 +- .../Modal/content/pickers/ImagePicker.tsx | 2 +- .../Modal/content/pickers/SoundPicker.tsx | 2 - .../content/pickers/WebFontInputModal.tsx | 2 +- .../Modal/content/pickers/pickerRowClass.ts | 4 +- .../Modal/content/settings/NoteSetting.tsx | 2 +- .../settings/ShortcutSettingsModal.tsx | 18 +- .../main/Modal/content/settings/TabList.tsx | 43 ++-- src/renderer/components/main/Settings.tsx | 15 +- .../components/main/Tool/CanvasTool.tsx | 2 +- .../components/main/Tool/SettingTool.tsx | 4 +- src/renderer/components/main/Tool/TabTool.tsx | 4 +- src/renderer/components/main/Tool/ToolBar.tsx | 4 +- .../components/main/common/Button.tsx | 45 ---- .../components/main/common/Dropdown.tsx | 22 +- .../components/main/common/IconButton.tsx | 40 --- src/renderer/components/main/common/Input.tsx | 24 -- src/renderer/components/main/common/Radio.tsx | 40 --- .../components/main/common/TabSwitch.tsx | 2 +- src/renderer/hooks/Grid/useGridKeyboard.ts | 3 +- src/renderer/hooks/useLenis.ts | 23 -- src/renderer/styles/global.css | 25 -- src/renderer/styles/main.css | 172 ++----------- src/renderer/styles/tokens.css | 7 + src/renderer/utils/plugin/pluginComponents.ts | 4 +- tailwind.config.js | 230 +++++++++--------- 53 files changed, 290 insertions(+), 1157 deletions(-) delete mode 100644 src/renderer/components/main/common/Button.tsx delete mode 100644 src/renderer/components/main/common/IconButton.tsx delete mode 100644 src/renderer/components/main/common/Input.tsx delete mode 100644 src/renderer/components/main/common/Radio.tsx diff --git a/src/renderer/components/main/Grid/PropertiesPanel.tsx b/src/renderer/components/main/Grid/PropertiesPanel.tsx index 31223632..771e8269 100644 --- a/src/renderer/components/main/Grid/PropertiesPanel.tsx +++ b/src/renderer/components/main/Grid/PropertiesPanel.tsx @@ -420,7 +420,21 @@ const PropertiesPanel: React.FC = ({ closePage(); }, [activeTab, panelMode, isPanelVisible, selectedKeyType, closePage]); - // Escape로 서브 페이지 닫기 — 입력 필드 편집과 경합 방지 + // 패널 본문 종류(단일/배치/플러그인 등)가 바뀌면 트리거 사이트가 함께 + // 사라지므로 서브 페이지 무효화 — 선택 이펙트의 early return 경로 보완 + const panelScopeKey = [ + pluginSettingsPanel ? 'plugin-settings' : 'grid', + selectedKeyElements.length, + selectedElements.length, + selectedPluginElements.length, + selectedGraphElements.length, + selectedKnobElements.length, + ].join('|'); + useEffect(() => { + closePage(); + }, [panelScopeKey, closePage]); + + // Escape로 서브 페이지 닫기 — 입력 필드 편집·상위 레이어와 경합 방지 useEffect(() => { if (!activePageKey) return; const onKey = (event: KeyboardEvent) => { @@ -434,10 +448,21 @@ const PropertiesPanel: React.FC = ({ ) { return; } + // 모달·포털 메뉴 등 상위 레이어가 열려 있으면 그쪽이 Escape를 소유 + if ( + document.querySelector( + '[data-dmn-modal-backdrop="true"], [data-dmn-popup-submenu="true"], [role="dialog"]', + ) + ) { + return; + } + // 이 레이어가 소비 — 그리드의 선택 해제까지 내려가지 않게 + event.preventDefault(); + event.stopPropagation(); closePage(); }; - document.addEventListener('keydown', onKey); - return () => document.removeEventListener('keydown', onKey); + document.addEventListener('keydown', onKey, true); + return () => document.removeEventListener('keydown', onKey, true); }, [activePageKey, closePage]); // 통계 요소 선택 시 NOTE 탭 숨김 처리 @@ -671,69 +696,10 @@ const PropertiesPanel: React.FC = ({ }, [selectedElements]); // 스크롤 훅 사용 - const { - batchScrollRefFor, - batchThumbRefFor, - singleScrollRefFor, - singleThumbRefFor, - } = usePanelScroll(activeTab, selectedElements.length); + const { batchScrollRefFor, singleScrollRefFor } = usePanelScroll(); // 플러그인 패널 스크롤 - const pluginScrollElementRef = useRef(null); - const pluginThumbRef = useRef(null); - - const calculatePluginThumb = (el: HTMLDivElement) => { - const { scrollTop, scrollHeight, clientHeight } = el; - const canScroll = scrollHeight > clientHeight + 1; - if (!canScroll) return { top: 0, height: 0, visible: false }; - - const minThumbHeight = 16; - const height = Math.max( - minThumbHeight, - (clientHeight / scrollHeight) * clientHeight, - ); - const maxTop = clientHeight - height; - const top = - maxTop <= 0 ? 0 : (scrollTop / (scrollHeight - clientHeight)) * maxTop; - - return { top, height, visible: true }; - }; - - const updatePluginThumbDOM = () => { - if (!pluginThumbRef.current || !pluginScrollElementRef.current) return; - const thumb = calculatePluginThumb(pluginScrollElementRef.current); - pluginThumbRef.current.style.top = `${thumb.top}px`; - pluginThumbRef.current.style.height = `${thumb.height}px`; - pluginThumbRef.current.style.display = thumb.visible ? 'block' : 'none'; - }; - - const { scrollContainerRef: pluginLenisRef } = useLenis({ - onScroll: updatePluginThumbDOM, - }); - - const setPluginScrollRef = (node: HTMLDivElement | null) => { - pluginScrollElementRef.current = node; - pluginLenisRef(node); - }; - - const setPluginThumbRef = (node: HTMLDivElement | null) => { - pluginThumbRef.current = node; - }; - - useEffect(() => { - const hasPluginPanel = - !!pluginSettingsPanel || - (selectedPluginElements.length > 0 && - selectedKeyLikeElements.length === 0 && - selectedGraphElements.length === 0); - - if (!hasPluginPanel) return; - - const raf = requestAnimationFrame(() => { - updatePluginThumbDOM(); - }); - return () => cancelAnimationFrame(raf); - }); + const { scrollContainerRef: setPluginScrollRef } = useLenis(); // 배치 편집용 로컬 ColorPicker 상태 type BatchPickerTarget = @@ -2641,7 +2607,6 @@ const PropertiesPanel: React.FC = ({ handlePluginSettingsPanelConfirm={handlePluginSettingsPanelConfirm} handlePluginSettingsPanelCancel={handlePluginSettingsPanelCancel} setPluginScrollRef={setPluginScrollRef} - setPluginThumbRef={setPluginThumbRef} renderPluginSettingsForm={renderPluginSettingsForm} t={t} /> @@ -2750,7 +2715,6 @@ const PropertiesPanel: React.FC = ({ handleBatchGlowColorChangeCompleteKeysOnly } batchScrollRefFor={batchScrollRefFor} - batchThumbRefFor={batchThumbRefFor} batchNoteColorButtonRef={batchNoteColorButtonRef} batchGlowColorButtonRef={batchGlowColorButtonRef} batchBorderColorButtonRef={batchBorderColorButtonRef} @@ -2819,7 +2783,6 @@ const PropertiesPanel: React.FC = ({ getMixedValueKnobsAsKey={getMixedValueKnobsAsKey} getSelectedKnobsData={getSelectedKnobsData} batchScrollRefFor={batchScrollRefFor} - batchThumbRefFor={batchThumbRefFor} batchImageButtonRef={batchImageButtonRef} showBatchImagePicker={showBatchImagePicker} setShowBatchImagePicker={setShowBatchImagePicker} @@ -2867,7 +2830,6 @@ const PropertiesPanel: React.FC = ({ getMixedValueGraphsAsKey={getMixedValueGraphsAsKey} getSelectedGraphsData={getSelectedGraphsData} batchScrollRefFor={batchScrollRefFor} - batchThumbRefFor={batchThumbRefFor} batchImageButtonRef={batchImageButtonRef} showBatchImagePicker={showBatchImagePicker} setShowBatchImagePicker={setShowBatchImagePicker} @@ -2898,7 +2860,6 @@ const PropertiesPanel: React.FC = ({ handleToggleMode={handleToggleMode} handleTogglePanel={handleTogglePanel} setPluginScrollRef={setPluginScrollRef} - setPluginThumbRef={setPluginThumbRef} isPluginResizable={isPluginResizable} selectedPluginElement={selectedPluginElement} pluginDisplaySize={pluginDisplaySize} @@ -2944,7 +2905,6 @@ const PropertiesPanel: React.FC = ({ handleToggleMode={handleToggleMode} handleTogglePanel={handleTogglePanel} singleScrollRefFor={singleScrollRefFor} - singleThumbRefFor={singleThumbRefFor} panelElement={panelElement} useCustomCSS={useCustomCSS} t={t} @@ -2977,7 +2937,6 @@ const PropertiesPanel: React.FC = ({ handleTogglePanel={handleTogglePanel} handleGraphUpdate={handleGraphUpdate} singleScrollRefFor={singleScrollRefFor} - singleThumbRefFor={singleThumbRefFor} showGraphImagePicker={showGraphImagePicker} setShowGraphImagePicker={setShowGraphImagePicker} graphImageButtonRef={graphImageButtonRef} @@ -3038,7 +2997,6 @@ const PropertiesPanel: React.FC = ({ panelElement={panelElement} useCustomCSS={useCustomCSS} singleScrollRefFor={singleScrollRefFor} - singleThumbRefFor={singleThumbRefFor} t={t} /> ); @@ -3053,10 +3011,12 @@ const PropertiesPanel: React.FC = ({ value={{ activePageKey, renderPageKey, openPage, closePage, pageHost }} >
+ {/* inert — 슬라이드 아웃된 레이어를 키보드 탭 순회·접근성 트리에서 제외 */}
{panelBody}
@@ -3065,6 +3025,7 @@ const PropertiesPanel: React.FC = ({ className="dmn-panel-page" data-page-depth="sub" data-active={activePageKey ? 'true' : 'false'} + inert={activePageKey ? undefined : true} />
diff --git a/src/renderer/components/main/Grid/PropertiesPanel/PluginSettingsPanelView.tsx b/src/renderer/components/main/Grid/PropertiesPanel/PluginSettingsPanelView.tsx index ab3a8e19..545bfb81 100644 --- a/src/renderer/components/main/Grid/PropertiesPanel/PluginSettingsPanelView.tsx +++ b/src/renderer/components/main/Grid/PropertiesPanel/PluginSettingsPanelView.tsx @@ -19,7 +19,6 @@ interface PluginSettingsPanelViewProps { handlePluginSettingsPanelConfirm: () => void; handlePluginSettingsPanelCancel: () => void; setPluginScrollRef: (node: HTMLDivElement | null) => void; - setPluginThumbRef: (node: HTMLDivElement | null) => void; renderPluginSettingsForm: ( schema: Record | undefined, values: Record, @@ -39,7 +38,6 @@ const PluginSettingsPanelView: React.FC = ({ handlePluginSettingsPanelConfirm, handlePluginSettingsPanelCancel, setPluginScrollRef, - setPluginThumbRef, renderPluginSettingsForm, t, }) => { @@ -76,13 +74,6 @@ const PluginSettingsPanelView: React.FC = ({ handlePluginSettingsPanelChange, )} -
-
-
diff --git a/src/renderer/components/main/Grid/PropertiesPanel/PropertyInputs.tsx b/src/renderer/components/main/Grid/PropertiesPanel/PropertyInputs.tsx index cc6c6368..8444d254 100644 --- a/src/renderer/components/main/Grid/PropertiesPanel/PropertyInputs.tsx +++ b/src/renderer/components/main/Grid/PropertiesPanel/PropertyInputs.tsx @@ -1,13 +1,11 @@ /* eslint-disable react-hooks/set-state-in-effect */ import React, { useState, useEffect, useRef } from 'react'; -import { createPortal } from 'react-dom'; import type { PropertyRowProps, NumberInputProps, OptionalNumberInputProps, TextInputProps, ColorInputProps, - SelectInputProps, ToggleSwitchProps, TabButtonProps, TabsProps, @@ -34,7 +32,7 @@ export const PropertyRow: React.FC = ({ export const PropertySection: React.FC<{ children: React.ReactNode }> = ({ children, }) => ( -
+
{children}
); @@ -760,127 +758,6 @@ export const ColorInput: React.FC = ({ ); }; -// ============================================================================ -// 선택 입력 -// ============================================================================ - -export const SelectInput: React.FC = ({ - value, - options, - onChange, -}) => { - const [isOpen, setIsOpen] = useState(false); - const [menuPos, setMenuPos] = useState<{ - left: number; - top: number; - width: number; - } | null>(null); - const triggerRef = useRef(null); - const menuRef = useRef(null); - - // 트리거가 스크롤/리사이즈로 움직이면 좌표가 어긋나므로 닫는다 - // (메뉴 내부 스크롤은 제외) - useEffect(() => { - if (!isOpen) return; - const close = (event?: Event) => { - if ( - event?.target instanceof Node && - menuRef.current?.contains(event.target) - ) { - return; - } - setIsOpen(false); - }; - window.addEventListener('scroll', close, true); - window.addEventListener('resize', close); - return () => { - window.removeEventListener('scroll', close, true); - window.removeEventListener('resize', close); - }; - }, [isOpen]); - - return ( -
- - {isOpen && - menuPos && - createPortal( - // 패널의 backdrop-filter·mask 아래에서는 중첩 backdrop-blur가 - // 무력화되므로 메뉴는 body로 포털해 backdrop root 밖에서 그린다 - <> -
setIsOpen(false)} - /> -
- {options.map((opt) => ( - - ))} -
- , - document.body, - )} -
- ); -}; - // ============================================================================ // 토글 스위치 // ============================================================================ @@ -1101,7 +978,7 @@ export const Tabs: React.FC = ({ const activeIndex = Math.max(0, tabs.indexOf(activeTab)); return ( -
+
void; // refs batchScrollRefFor: (tab: TabType) => (node: HTMLDivElement | null) => void; - batchThumbRefFor: (tab: TabType) => (node: HTMLDivElement | null) => void; batchNoteColorButtonRef: React.RefObject; batchGlowColorButtonRef: React.RefObject; batchBorderColorButtonRef: React.RefObject; @@ -251,7 +250,6 @@ export const BatchKeyLikePanel: React.FC = ({ handleBatchGlowColorChange: _handleBatchGlowColorChange, handleBatchGlowColorChangeComplete: _handleBatchGlowColorChangeComplete, batchScrollRefFor, - batchThumbRefFor, batchNoteColorButtonRef, batchGlowColorButtonRef, batchBorderColorButtonRef, @@ -766,13 +764,6 @@ export const BatchKeyLikePanel: React.FC = ({ t={t} />
-
-
-
{/* NOTE 탭 viewport */} @@ -810,13 +801,6 @@ export const BatchKeyLikePanel: React.FC = ({ t={t} />
-
-
-
)} @@ -843,13 +827,6 @@ export const BatchKeyLikePanel: React.FC = ({ t={t} />
-
-
-
@@ -1022,7 +999,6 @@ interface BatchGraphOnlyPanelProps { getMixedValueGraphsAsKey: MixedValueGetter; getSelectedGraphsData: () => KeyData[]; batchScrollRefFor: (tab: TabType) => (node: HTMLDivElement | null) => void; - batchThumbRefFor: (tab: TabType) => (node: HTMLDivElement | null) => void; batchImageButtonRef: React.RefObject; showBatchImagePicker: boolean; setShowBatchImagePicker: (value: boolean) => void; @@ -1060,7 +1036,6 @@ export const BatchGraphOnlyPanel: React.FC = ({ getMixedValueGraphsAsKey, getSelectedGraphsData, batchScrollRefFor, - batchThumbRefFor, batchImageButtonRef, showBatchImagePicker, setShowBatchImagePicker, @@ -1313,13 +1288,6 @@ export const BatchGraphOnlyPanel: React.FC = ({ t={t} />
-
-
-
@@ -1412,7 +1380,6 @@ interface BatchKnobOnlyPanelProps { getMixedValueKnobsAsKey: MixedValueGetter; getSelectedKnobsData: () => KeyData[]; batchScrollRefFor: (tab: TabType) => (node: HTMLDivElement | null) => void; - batchThumbRefFor: (tab: TabType) => (node: HTMLDivElement | null) => void; batchImageButtonRef: React.RefObject; showBatchImagePicker: boolean; setShowBatchImagePicker: (value: boolean) => void; @@ -1450,7 +1417,6 @@ export const BatchKnobOnlyPanel: React.FC = ({ getMixedValueKnobsAsKey, getSelectedKnobsData, batchScrollRefFor, - batchThumbRefFor, batchImageButtonRef, showBatchImagePicker, setShowBatchImagePicker, @@ -1622,13 +1588,6 @@ export const BatchKnobOnlyPanel: React.FC = ({ t={t} /> -
-
-
diff --git a/src/renderer/components/main/Grid/PropertiesPanel/index.ts b/src/renderer/components/main/Grid/PropertiesPanel/index.ts index 9b16055e..c4bbc40c 100644 --- a/src/renderer/components/main/Grid/PropertiesPanel/index.ts +++ b/src/renderer/components/main/Grid/PropertiesPanel/index.ts @@ -9,7 +9,6 @@ export { OptionalNumberInput, TextInput, ColorInput, - SelectInput, ToggleSwitch, FontStyleToggle, Tabs, diff --git a/src/renderer/components/main/Grid/PropertiesPanel/layer/LayerPanel.tsx b/src/renderer/components/main/Grid/PropertiesPanel/layer/LayerPanel.tsx index f539dc74..b3928f10 100644 --- a/src/renderer/components/main/Grid/PropertiesPanel/layer/LayerPanel.tsx +++ b/src/renderer/components/main/Grid/PropertiesPanel/layer/LayerPanel.tsx @@ -62,7 +62,7 @@ const LayerPanelTabs: React.FC = ({ const activeIndex = activeTab === LAYER_PANEL_TABS.GRID ? 1 : 0; return ( -
+
= ({ return () => clearPendingDeselect(); }); - // 스크롤 상태 + // 스크롤 상태 — scrollElementRef는 DnD 훅의 오토스크롤 계산용 const scrollElementRef = useRef(null); - const thumbRef = useRef(null); - const calculateThumb = (el: HTMLDivElement) => { - const { scrollTop, scrollHeight, clientHeight } = el; - const canScroll = scrollHeight > clientHeight + 1; - if (!canScroll) return { top: 0, height: 0, visible: false }; - - const minThumbHeight = 16; - const height = Math.max( - minThumbHeight, - (clientHeight / scrollHeight) * clientHeight, - ); - const maxTop = clientHeight - height; - const top = - maxTop <= 0 ? 0 : (scrollTop / (scrollHeight - clientHeight)) * maxTop; - - return { top, height, visible: true }; - }; - - const updateThumbDOM = () => { - if (!thumbRef.current || !scrollElementRef.current) return; - const thumb = calculateThumb(scrollElementRef.current); - thumbRef.current.style.top = `${thumb.top}px`; - thumbRef.current.style.height = `${thumb.height}px`; - thumbRef.current.style.display = thumb.visible ? 'block' : 'none'; - }; - - const { scrollContainerRef: lenisRef, lenisInstance } = useLenis({ - onScroll: updateThumbDOM, - }); + const { scrollContainerRef: lenisRef, lenisInstance } = useLenis(); const setScrollRef = (node: HTMLDivElement | null) => { scrollElementRef.current = node; lenisRef(node); }; - useEffect(() => { - updateThumbDOM(); - }); - // 레이어 아이템 목록 const layerItems = buildLayerItems({ selectedKeyType, @@ -180,7 +148,6 @@ const LayerTabContent: React.FC = ({ useEffect(() => { const rafId = requestAnimationFrame(() => { lenisInstance.current?.resize(); - updateThumbDOM(); }); return () => cancelAnimationFrame(rafId); }); @@ -645,7 +612,7 @@ const LayerTabContent: React.FC = ({ e.stopPropagation(); actions.handleToggleGroupVisibility(e, gh.groupId); }} - className={`flex-shrink-0 w-[28px] h-[28px] flex items-center justify-center rounded-[6px] cursor-pointer ${ + className={`flex-shrink-0 w-[28px] h-[28px] flex items-center justify-center rounded-md cursor-pointer ${ gh.allHidden ? '' : 'opacity-0 group-hover:opacity-60 hover:!opacity-100' @@ -783,7 +750,7 @@ const LayerTabContent: React.FC = ({ ? t('propertiesPanel.showLayer') || 'Show' : t('propertiesPanel.hideLayer') || 'Hide' } - className={`flex-shrink-0 w-[28px] h-[28px] flex items-center justify-center rounded-[6px] cursor-pointer ${ + className={`flex-shrink-0 w-[28px] h-[28px] flex items-center justify-center rounded-md cursor-pointer ${ item.hidden ? '' : 'opacity-0 group-hover:opacity-60 hover:!opacity-100' @@ -814,15 +781,6 @@ const LayerTabContent: React.FC = ({ )}
)} - - {/* 커스텀 스크롤바 */} -
-
-
{/* 컨텍스트 메뉴 */} diff --git a/src/renderer/components/main/Grid/PropertiesPanel/single/SingleSelectionPanel.tsx b/src/renderer/components/main/Grid/PropertiesPanel/single/SingleSelectionPanel.tsx index 8a149e8e..2b82bdaf 100644 --- a/src/renderer/components/main/Grid/PropertiesPanel/single/SingleSelectionPanel.tsx +++ b/src/renderer/components/main/Grid/PropertiesPanel/single/SingleSelectionPanel.tsx @@ -85,7 +85,6 @@ interface PluginSelectionPanelProps { handleToggleMode: () => void; handleTogglePanel: () => void; setPluginScrollRef: (node: HTMLDivElement | null) => void; - setPluginThumbRef: (node: HTMLDivElement | null) => void; isPluginResizable: boolean; selectedPluginElement: PluginDisplayElementInternal | null; pluginDisplaySize: { width: number; height: number }; @@ -116,7 +115,6 @@ export const PluginSelectionPanel: React.FC = ({ handleToggleMode, handleTogglePanel, setPluginScrollRef, - setPluginThumbRef, isPluginResizable, selectedPluginElement, pluginDisplaySize, @@ -233,13 +231,6 @@ export const PluginSelectionPanel: React.FC = ({ )}
-
-
-
@@ -269,7 +260,6 @@ interface SingleGraphPanelProps { data: Partial & { index: number }, ) => void; singleScrollRefFor: (tab: TabType) => (node: HTMLDivElement | null) => void; - singleThumbRefFor: (tab: TabType) => (node: HTMLDivElement | null) => void; showGraphImagePicker: boolean; setShowGraphImagePicker: (value: boolean) => void; graphImageButtonRef: React.RefObject; @@ -297,7 +287,6 @@ export const SingleGraphPanel: React.FC = ({ handleTogglePanel, handleGraphUpdate, singleScrollRefFor, - singleThumbRefFor, showGraphImagePicker, setShowGraphImagePicker, graphImageButtonRef, @@ -659,13 +648,6 @@ export const SingleGraphPanel: React.FC = ({ )} -
-
-
@@ -766,7 +748,6 @@ interface SingleKnobPanelProps { handleToggleMode: () => void; handleTogglePanel: () => void; singleScrollRefFor: (tab: TabType) => (node: HTMLDivElement | null) => void; - singleThumbRefFor: (tab: TabType) => (node: HTMLDivElement | null) => void; panelElement: HTMLDivElement | null; useCustomCSS: boolean; t: (key: string) => string; @@ -789,7 +770,6 @@ export const SingleKnobPanel: React.FC = ({ handleToggleMode, handleTogglePanel, singleScrollRefFor, - singleThumbRefFor, panelElement, useCustomCSS, t, @@ -1246,13 +1226,6 @@ export const SingleKnobPanel: React.FC = ({ )} -
-
-
@@ -1390,7 +1363,6 @@ interface SingleKeyStatPanelProps { panelElement: HTMLDivElement | null; useCustomCSS: boolean; singleScrollRefFor: (tab: TabType) => (node: HTMLDivElement | null) => void; - singleThumbRefFor: (tab: TabType) => (node: HTMLDivElement | null) => void; t: (key: string) => string | undefined; } @@ -1434,7 +1406,6 @@ export const SingleKeyStatPanel: React.FC = ({ panelElement, useCustomCSS, singleScrollRefFor, - singleThumbRefFor, t, }) => { const availableTabs = isSingleStat @@ -1661,13 +1632,6 @@ export const SingleKeyStatPanel: React.FC = ({ onSizeBlur={handleSizeBlur} /> -
-
-
{/* NOTE 탭 viewport */} @@ -1688,13 +1652,6 @@ export const SingleKeyStatPanel: React.FC = ({ t={t} /> -
-
-
)} @@ -1716,13 +1673,6 @@ export const SingleKeyStatPanel: React.FC = ({ t={t} /> -
-
-
diff --git a/src/renderer/components/main/Grid/PropertiesPanel/types.ts b/src/renderer/components/main/Grid/PropertiesPanel/types.ts index 4c9f567c..78f9a62f 100644 --- a/src/renderer/components/main/Grid/PropertiesPanel/types.ts +++ b/src/renderer/components/main/Grid/PropertiesPanel/types.ts @@ -87,12 +87,6 @@ export interface ColorInputProps { onToggle?: () => void; } -export interface SelectInputProps { - value: string; - options: { value: string; label: string }[]; - onChange: (value: string) => void; -} - export interface ToggleSwitchProps { checked: boolean; onChange: (checked: boolean) => void; diff --git a/src/renderer/components/main/Grid/PropertiesPanel/usePanelScroll.ts b/src/renderer/components/main/Grid/PropertiesPanel/usePanelScroll.ts index 2a0e772e..acf83424 100644 --- a/src/renderer/components/main/Grid/PropertiesPanel/usePanelScroll.ts +++ b/src/renderer/components/main/Grid/PropertiesPanel/usePanelScroll.ts @@ -1,140 +1,24 @@ -import { useCallback, useEffect, useRef } from 'react'; +import { useCallback } from 'react'; import { useLenis } from '@hooks/useLenis'; import { TABS, TabType } from './types'; -interface ScrollThumbState { - top: number; - height: number; - visible: boolean; -} - interface UsePanelScrollReturn { - // 일괄 스크롤 ref + // 탭별 keepalive 뷰포트에 Lenis 스무스 스크롤 연결 batchScrollRefFor: (tab: TabType) => (node: HTMLDivElement | null) => void; - batchThumbRefFor: (tab: TabType) => (node: HTMLDivElement | null) => void; - // 단일 스크롤 ref singleScrollRefFor: (tab: TabType) => (node: HTMLDivElement | null) => void; - singleThumbRefFor: (tab: TabType) => (node: HTMLDivElement | null) => void; - // 수동 thumb 업데이트 - updateThumbs: (tab: TabType) => void; } -export function usePanelScroll( - activeTab: TabType, - selectedElementsLength: number, -): UsePanelScrollReturn { - // Thumb refs (탭별, 직접 DOM 조작으로 리렌더링 방지) - const batchThumbRefs = useRef>({ - [TABS.STYLE]: null, - [TABS.NOTE]: null, - [TABS.COUNTER]: null, - }); - const singleThumbRefs = useRef>({ - [TABS.STYLE]: null, - [TABS.NOTE]: null, - [TABS.COUNTER]: null, - }); - - // Scroll element refs (탭별, thumb 계산용) - const batchScrollElementRefs = useRef>( - { - [TABS.STYLE]: null, - [TABS.NOTE]: null, - [TABS.COUNTER]: null, - }, - ); - const singleScrollElementRefs = useRef< - Record - >({ - [TABS.STYLE]: null, - [TABS.NOTE]: null, - [TABS.COUNTER]: null, - }); - - const calculateThumb = useCallback((el: HTMLDivElement): ScrollThumbState => { - const { scrollTop, scrollHeight, clientHeight } = el; - const canScroll = scrollHeight > clientHeight + 1; - if (!canScroll) return { top: 0, height: 0, visible: false }; - - const minThumbHeight = 16; - const height = Math.max( - minThumbHeight, - (clientHeight / scrollHeight) * clientHeight, - ); - const maxTop = clientHeight - height; - const top = - maxTop <= 0 ? 0 : (scrollTop / (scrollHeight - clientHeight)) * maxTop; - - return { top, height, visible: true }; - }, []); - - // thumb DOM 직접 업데이트 (리렌더링 없이 성능 최적화) - const updateThumbDOM = useCallback( - (thumbEl: HTMLDivElement | null, scrollEl: HTMLDivElement | null) => { - if (!thumbEl || !scrollEl) return; - const thumb = calculateThumb(scrollEl); - thumbEl.style.top = `${thumb.top}px`; - thumbEl.style.height = `${thumb.height}px`; - thumbEl.style.display = thumb.visible ? 'block' : 'none'; - }, - [calculateThumb], - ); - - // Lenis 스크롤 적용 (탭별 6개 훅: batch 3개 + single 3개) - const { scrollContainerRef: batchLenisStyleRef } = useLenis({ - onScroll: useCallback(() => { - updateThumbDOM( - batchThumbRefs.current[TABS.STYLE], - batchScrollElementRefs.current[TABS.STYLE], - ); - }, [updateThumbDOM]), - }); - const { scrollContainerRef: batchLenisNoteRef } = useLenis({ - onScroll: useCallback(() => { - updateThumbDOM( - batchThumbRefs.current[TABS.NOTE], - batchScrollElementRefs.current[TABS.NOTE], - ); - }, [updateThumbDOM]), - }); - const { scrollContainerRef: batchLenisCounterRef } = useLenis({ - onScroll: useCallback(() => { - updateThumbDOM( - batchThumbRefs.current[TABS.COUNTER], - batchScrollElementRefs.current[TABS.COUNTER], - ); - }, [updateThumbDOM]), - }); - - const { scrollContainerRef: singleLenisStyleRef } = useLenis({ - onScroll: useCallback(() => { - updateThumbDOM( - singleThumbRefs.current[TABS.STYLE], - singleScrollElementRefs.current[TABS.STYLE], - ); - }, [updateThumbDOM]), - }); - const { scrollContainerRef: singleLenisNoteRef } = useLenis({ - onScroll: useCallback(() => { - updateThumbDOM( - singleThumbRefs.current[TABS.NOTE], - singleScrollElementRefs.current[TABS.NOTE], - ); - }, [updateThumbDOM]), - }); - const { scrollContainerRef: singleLenisCounterRef } = useLenis({ - onScroll: useCallback(() => { - updateThumbDOM( - singleThumbRefs.current[TABS.COUNTER], - singleScrollElementRefs.current[TABS.COUNTER], - ); - }, [updateThumbDOM]), - }); +export function usePanelScroll(): UsePanelScrollReturn { + // 탭별 6개 인스턴스 (batch 3 + single 3) — 뷰포트가 keepalive라 각자 유지 + const { scrollContainerRef: batchLenisStyleRef } = useLenis(); + const { scrollContainerRef: batchLenisNoteRef } = useLenis(); + const { scrollContainerRef: batchLenisCounterRef } = useLenis(); + const { scrollContainerRef: singleLenisStyleRef } = useLenis(); + const { scrollContainerRef: singleLenisNoteRef } = useLenis(); + const { scrollContainerRef: singleLenisCounterRef } = useLenis(); - // callback ref를 합성하여 Lenis와 내부 ref 모두 업데이트 (탭별) const batchScrollRefFor = useCallback( (tab: TabType) => (node: HTMLDivElement | null) => { - batchScrollElementRefs.current[tab] = node; if (tab === TABS.STYLE) batchLenisStyleRef(node); if (tab === TABS.NOTE) batchLenisNoteRef(node); if (tab === TABS.COUNTER) batchLenisCounterRef(node); @@ -144,7 +28,6 @@ export function usePanelScroll( const singleScrollRefFor = useCallback( (tab: TabType) => (node: HTMLDivElement | null) => { - singleScrollElementRefs.current[tab] = node; if (tab === TABS.STYLE) singleLenisStyleRef(node); if (tab === TABS.NOTE) singleLenisNoteRef(node); if (tab === TABS.COUNTER) singleLenisCounterRef(node); @@ -152,44 +35,5 @@ export function usePanelScroll( [singleLenisStyleRef, singleLenisNoteRef, singleLenisCounterRef], ); - const batchThumbRefFor = useCallback( - (tab: TabType) => (node: HTMLDivElement | null) => { - batchThumbRefs.current[tab] = node; - }, - [], - ); - - const singleThumbRefFor = useCallback( - (tab: TabType) => (node: HTMLDivElement | null) => { - singleThumbRefs.current[tab] = node; - }, - [], - ); - - const updateThumbs = useCallback( - (tab: TabType) => { - updateThumbDOM( - batchThumbRefs.current[tab], - batchScrollElementRefs.current[tab], - ); - updateThumbDOM( - singleThumbRefs.current[tab], - singleScrollElementRefs.current[tab], - ); - }, - [updateThumbDOM], - ); - - // 탭 변경 또는 선택 변경 시 thumb 업데이트 - useEffect(() => { - updateThumbs(activeTab); - }, [updateThumbs, activeTab, selectedElementsLength]); - - return { - batchScrollRefFor, - batchThumbRefFor, - singleScrollRefFor, - singleThumbRefFor, - updateThumbs, - }; + return { batchScrollRefFor, singleScrollRefFor }; } diff --git a/src/renderer/components/main/Grid/core/GridMinimap.tsx b/src/renderer/components/main/Grid/core/GridMinimap.tsx index 0f399ebe..8792a9d7 100644 --- a/src/renderer/components/main/Grid/core/GridMinimap.tsx +++ b/src/renderer/components/main/Grid/core/GridMinimap.tsx @@ -61,7 +61,7 @@ const ZoomButton = ({ onClick, title, style, children }: ZoomButtonProps) => ( ...style, }} onMouseEnter={(e) => - (e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.1)') + (e.currentTarget.style.backgroundColor = 'var(--ui-fill-active)') } onMouseLeave={(e) => (e.currentTarget.style.backgroundColor = 'transparent') diff --git a/src/renderer/components/main/Modal/FloatingPopup.tsx b/src/renderer/components/main/Modal/FloatingPopup.tsx index 1ec3892e..fed0c6b9 100644 --- a/src/renderer/components/main/Modal/FloatingPopup.tsx +++ b/src/renderer/components/main/Modal/FloatingPopup.tsx @@ -67,7 +67,11 @@ const FloatingPopup = ({ useEffect(() => { if (open && autoClose) { const onKey = (e: KeyboardEvent) => { - if (e.key === 'Escape') onClose?.(); + if (e.key === 'Escape') { + // 이 레이어가 소비 — 하위 레이어(페이지·그리드 선택)로 내려가지 않게 + e.preventDefault(); + onClose?.(); + } }; const onClickAway = (e: MouseEvent) => { diff --git a/src/renderer/components/main/Modal/ListPopup.tsx b/src/renderer/components/main/Modal/ListPopup.tsx index ca7366a5..7e443d1c 100644 --- a/src/renderer/components/main/Modal/ListPopup.tsx +++ b/src/renderer/components/main/Modal/ListPopup.tsx @@ -124,7 +124,7 @@ const SubMenu = ({ data-dmn-popup-submenu="true" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} - className={`fixed z-[10001] bg-glass backdrop-blur-[24px] shadow-elevation-2 rounded-[10px] p-[4px] flex flex-col gap-[4px] tooltip-fade-in${ + className={`fixed z-[10001] bg-glass backdrop-blur-[24px] shadow-elevation-2 rounded-surface p-[4px] flex flex-col gap-[4px] tooltip-fade-in${ needsScroll ? ' listpopup-scroll' : '' }`} style={{ @@ -247,7 +247,7 @@ const MenuItemRow = ({ type="button" disabled={item.disabled} onClick={handleSelect} - className={`w-full min-w-[108px] h-[26px] px-[24px] rounded-[6px] flex items-center justify-center transition-colors duration-fast ${ + className={`w-full min-w-[108px] h-[26px] px-[24px] rounded-md flex items-center justify-center transition-colors duration-fast ${ item.disabled ? 'opacity-70' : 'hover:bg-surface-hover active:bg-surface-active cursor-pointer' @@ -275,7 +275,7 @@ const MenuItemRow = ({ type="button" disabled={item.disabled} onClick={handleSelect} - className={`w-full min-w-[96px] h-[26px] px-[8px] rounded-[6px] flex items-center gap-[6px] transition-colors duration-fast ${ + className={`w-full min-w-[96px] h-[26px] px-[8px] rounded-md flex items-center gap-[6px] transition-colors duration-fast ${ item.disabled ? 'opacity-70' : 'hover:bg-surface-hover active:bg-surface-active cursor-pointer' @@ -368,7 +368,7 @@ const ListPopup = ({ }: ListPopupProps) => { // 일시적 팝업은 상주 크롬(z-30, 패널·미니맵)보다 항상 위 const defaultClassName = - 'z-40 bg-glass backdrop-blur-[24px] shadow-elevation-2 rounded-[10px] p-[4px] flex flex-col gap-[4px]'; + 'z-40 bg-glass backdrop-blur-[24px] shadow-elevation-2 rounded-surface p-[4px] flex flex-col gap-[4px]'; const effectiveClassName = `${defaultClassName} ${className}`.trim(); // 스크롤 필요 여부 계산 (아이템 26 + 갭 4 리듬) diff --git a/src/renderer/components/main/Modal/ManagerModalLayout.tsx b/src/renderer/components/main/Modal/ManagerModalLayout.tsx index c3492aa7..bd670d59 100644 --- a/src/renderer/components/main/Modal/ManagerModalLayout.tsx +++ b/src/renderer/components/main/Modal/ManagerModalLayout.tsx @@ -113,7 +113,7 @@ const ManagerModalLayout = ({ return (
e.stopPropagation()} > {/* 탭 영역 */} diff --git a/src/renderer/components/main/Modal/Modal.tsx b/src/renderer/components/main/Modal/Modal.tsx index 9d9784b7..a936ef4e 100644 --- a/src/renderer/components/main/Modal/Modal.tsx +++ b/src/renderer/components/main/Modal/Modal.tsx @@ -8,9 +8,7 @@ interface ModalProps { } const Modal = ({ onClick, children, animate = true }: ModalProps) => { - const backdropAnimClass = animate - ? 'opacity-0 animate-modal-fade' - : 'opacity-100'; + const scrimAnimClass = animate ? 'animate-modal-scrim' : ''; const contentAnimClass = animate ? 'animate-modal-scale' : ''; const closeFromBackdropRef = useRef(false); @@ -44,15 +42,25 @@ const Modal = ({ onClick, children, animate = true }: ModalProps) => { e.stopPropagation(); }; + // 배경 블러는 모달의 조상이 아니라 형제 언더레이가 소유해야 함 — + // 조상이 backdrop-filter를 가지면 backdrop root가 생겨 모달 글래스의 + // 블러가 WebKit에서 배경을 샘플링하지 못함. 형제 레이어는 정상 합성됨 + // 래퍼 opacity 애니메이션도 같은 이유로 금지 — opacity < 1인 조상이 + // backdrop root가 되어 페이드 동안 블러가 죽었다가 끝나는 순간 튐. + // 등장 모션은 스크림(틴트+블러 키프레임)과 콘텐츠가 각자 소유한다 return createPortal(
-
{children}
+ {/* 스크림 언더레이 — 클릭은 래퍼로 통과 */} +
+
{children}
, document.body, ); diff --git a/src/renderer/components/main/Modal/content/dialogs/Alert.tsx b/src/renderer/components/main/Modal/content/dialogs/Alert.tsx index 8fdb0c79..3456e473 100644 --- a/src/renderer/components/main/Modal/content/dialogs/Alert.tsx +++ b/src/renderer/components/main/Modal/content/dialogs/Alert.tsx @@ -48,11 +48,7 @@ const Alert = ({ }; // Lenis smooth scroll 적용 (onScroll 콜백으로 그림자 업데이트) - const { - scrollContainerRef: scrollRef, - wrapperElement, - scrollbarWidth, - } = useLenis({ + const { scrollContainerRef: scrollRef, wrapperElement } = useLenis({ onScroll: () => updateScrollState(wrapperElement), }); @@ -73,14 +69,10 @@ const Alert = ({ const cancelLabel = cancelText || t('common.cancel'); const shouldShowCancel = isConfirm || (isCustom && showCancel); - const hasOverflow = - !!wrapperElement && - wrapperElement.scrollHeight > wrapperElement.clientHeight + 1; - return (
e.stopPropagation()} > {/* 메시지 텍스트 or Custom HTML */} @@ -96,20 +88,6 @@ const Alert = ({
0 - ? `calc(100% + ${scrollbarWidth}px)` - : undefined, - transform: - hasOverflow && scrollbarWidth > 0 - ? `translateX(-${scrollbarWidth}px)` - : undefined, - paddingLeft: - hasOverflow && scrollbarWidth > 0 - ? `${scrollbarWidth}px` - : undefined, - }} dangerouslySetInnerHTML={{ __html: message }} /> diff --git a/src/renderer/components/main/Modal/content/dialogs/Laboratory.tsx b/src/renderer/components/main/Modal/content/dialogs/Laboratory.tsx index 313f6cb7..1562d333 100644 --- a/src/renderer/components/main/Modal/content/dialogs/Laboratory.tsx +++ b/src/renderer/components/main/Modal/content/dialogs/Laboratory.tsx @@ -83,7 +83,7 @@ const LaboratoryModal = ({ return (
e.stopPropagation()} >
diff --git a/src/renderer/components/main/Modal/content/dialogs/PluginDataDeleteModal.tsx b/src/renderer/components/main/Modal/content/dialogs/PluginDataDeleteModal.tsx index 559eacdb..92bfb244 100644 --- a/src/renderer/components/main/Modal/content/dialogs/PluginDataDeleteModal.tsx +++ b/src/renderer/components/main/Modal/content/dialogs/PluginDataDeleteModal.tsx @@ -23,13 +23,10 @@ export function PluginDataDeleteModal({ return (
event.stopPropagation()} >
- {/* - {t("settings.pluginDataDeleteTitle")} - */} {t('settings.pluginDataDeleteMessage', { name: pluginName })} diff --git a/src/renderer/components/main/Modal/content/dialogs/UnifiedKeySetting.tsx b/src/renderer/components/main/Modal/content/dialogs/UnifiedKeySetting.tsx index 4fccc622..a407b68a 100644 --- a/src/renderer/components/main/Modal/content/dialogs/UnifiedKeySetting.tsx +++ b/src/renderer/components/main/Modal/content/dialogs/UnifiedKeySetting.tsx @@ -92,7 +92,6 @@ const UnifiedKeySetting: React.FC = ({ scrollContainerRef: scrollRef, wrapperElement, lenisInstance, - scrollbarWidth, } = useLenis({ onScroll: () => updateScrollState(wrapperElement), }); @@ -234,7 +233,7 @@ const UnifiedKeySetting: React.FC = ({ return (
e.stopPropagation()} >
@@ -268,18 +267,6 @@ const UnifiedKeySetting: React.FC = ({ height: containerHeight !== null ? `${containerHeight}px` : 'auto', maxHeight: '195px', - width: - hasOverflow && scrollbarWidth > 0 - ? `calc(100% + ${scrollbarWidth}px)` - : undefined, - transform: - hasOverflow && scrollbarWidth > 0 - ? `translateX(-${scrollbarWidth}px)` - : undefined, - paddingLeft: - hasOverflow && scrollbarWidth > 0 - ? `${scrollbarWidth}px` - : undefined, transition: isFirstRender.current ? 'none' : 'height 100ms ease-in-out', diff --git a/src/renderer/components/main/Modal/content/dialogs/UpdateModal.tsx b/src/renderer/components/main/Modal/content/dialogs/UpdateModal.tsx index 59373eec..41926333 100644 --- a/src/renderer/components/main/Modal/content/dialogs/UpdateModal.tsx +++ b/src/renderer/components/main/Modal/content/dialogs/UpdateModal.tsx @@ -81,7 +81,7 @@ const UpdateModal = ({ return (
e.stopPropagation()} > {isLatestVersion ? ( diff --git a/src/renderer/components/main/Modal/content/editors/CounterAnimationEditorModal.tsx b/src/renderer/components/main/Modal/content/editors/CounterAnimationEditorModal.tsx index 1090b3f6..06fc0462 100644 --- a/src/renderer/components/main/Modal/content/editors/CounterAnimationEditorModal.tsx +++ b/src/renderer/components/main/Modal/content/editors/CounterAnimationEditorModal.tsx @@ -871,7 +871,7 @@ const CounterAnimationEditorModal = ({ />
-
+
-
+
{previewCss && ( diff --git a/src/renderer/components/main/Modal/content/editors/TabCssModal.tsx b/src/renderer/components/main/Modal/content/editors/TabCssModal.tsx index b9cbf174..518bc8ea 100644 --- a/src/renderer/components/main/Modal/content/editors/TabCssModal.tsx +++ b/src/renderer/components/main/Modal/content/editors/TabCssModal.tsx @@ -137,7 +137,7 @@ const TabCssModal = ({ isOpen, onClose, showAlert }: TabCssModalProps) => { return (
e.stopPropagation()} > {/* CSS 사용 여부 토글 */} @@ -156,7 +156,7 @@ const TabCssModal = ({ isOpen, onClose, showAlert }: TabCssModalProps) => { disabled={isLoading || !hasTabCss} className={`px-[8px] h-[23px] rounded-md transition-colors duration-fast flex items-center justify-center text-style-4 ${ hasTabCss - ? 'bg-danger-muted hover:bg-[rgba(229,72,77,0.2)] active:bg-[rgba(229,72,77,0.26)] text-danger-fg' + ? 'bg-danger-muted hover:bg-danger-muted-hover active:bg-danger-muted-active text-danger-fg' : 'bg-fill-faint text-fg-disabled cursor-not-allowed' }`} > diff --git a/src/renderer/components/main/Modal/content/editors/TabNameModal.tsx b/src/renderer/components/main/Modal/content/editors/TabNameModal.tsx index eb4e9d40..04197855 100644 --- a/src/renderer/components/main/Modal/content/editors/TabNameModal.tsx +++ b/src/renderer/components/main/Modal/content/editors/TabNameModal.tsx @@ -64,7 +64,7 @@ const TabNameModal = ({ return (
e.stopPropagation()} >
{t('tabs.createTitle')}
diff --git a/src/renderer/components/main/Modal/content/managers/SoundTrimModal.tsx b/src/renderer/components/main/Modal/content/managers/SoundTrimModal.tsx index 8209f4d7..90308432 100644 --- a/src/renderer/components/main/Modal/content/managers/SoundTrimModal.tsx +++ b/src/renderer/components/main/Modal/content/managers/SoundTrimModal.tsx @@ -1016,7 +1016,7 @@ const SoundTrimModal = ({ return (
event.stopPropagation()} > {/* 헤더 바 */} diff --git a/src/renderer/components/main/Modal/content/pickers/ColorPicker.tsx b/src/renderer/components/main/Modal/content/pickers/ColorPicker.tsx index 2515cd3e..f1f38693 100644 --- a/src/renderer/components/main/Modal/content/pickers/ColorPicker.tsx +++ b/src/renderer/components/main/Modal/content/pickers/ColorPicker.tsx @@ -782,7 +782,7 @@ const ColorPickerWrapper = ({ >
{ panelElement?: HTMLElement | null; onClose: () => void; interactiveRefs?: Array>; - widthClass?: string; - estimatedWidth?: number; estimatedHeight?: number; searchQuery: string; onSearchQueryChange: (value: string) => void; @@ -36,7 +37,6 @@ interface CommonListPickerPopupProps { isLoading?: boolean; loadingText?: string; errorText?: string; - listHeightClass?: string; onAdd: (event: React.MouseEvent) => void; addLabel: string; // 추가 버튼에 앵커된 메뉴가 열릴 때 외부 클릭 판정 제외용 @@ -53,8 +53,6 @@ export default function CommonListPickerPopup({ panelElement = null, onClose, interactiveRefs = [], - widthClass = 'w-[156px]', - estimatedWidth = 164, estimatedHeight = 280, searchQuery, onSearchQueryChange, @@ -68,7 +66,6 @@ export default function CommonListPickerPopup({ isLoading = false, loadingText = '로딩...', errorText = '', - listHeightClass = 'h-[120px]', onAdd, addLabel, addButtonRef, @@ -89,7 +86,7 @@ export default function CommonListPickerPopup({ const panelRect = panelElement.getBoundingClientRect(); const popupEl = containerRef.current; - const popupWidth = popupEl ? popupEl.offsetWidth : estimatedWidth; + const popupWidth = popupEl ? popupEl.offsetWidth : ESTIMATED_WIDTH; const popupHeight = popupEl ? popupEl.offsetHeight : estimatedHeight; const gap = 5; @@ -110,7 +107,7 @@ export default function CommonListPickerPopup({ if (prev && prev.x === fixedX && prev.y === fixedY) return prev; return { x: fixedX, y: fixedY }; }); - }, [estimatedHeight, estimatedWidth, panelElement]); + }, [estimatedHeight, panelElement]); useLayoutEffect(() => { if (!open) { @@ -180,7 +177,7 @@ export default function CommonListPickerPopup({ value={searchQuery} onChange={(event) => onSearchQueryChange(event.target.value)} placeholder={searchPlaceholder} - className="w-full h-[30px] pl-[30px] pr-[10px] bg-inset rounded-[10px] text-fg text-body placeholder-fg-faint focus:shadow-focus-ring outline-none transition-shadow duration-fast" + className="w-full h-[30px] pl-[30px] pr-[10px] bg-inset rounded-surface text-fg text-body placeholder-fg-faint focus:shadow-focus-ring outline-none transition-shadow duration-fast" />
) : ( @@ -196,7 +193,7 @@ export default function CommonListPickerPopup({ // 필터 + 추가 — 같은 재질의 칩 한 쌍. 페이지는 검색과 같은 30 크롬 스케일 const controlChipClass = renderMode === 'page' - ? 'w-[30px] h-[30px] rounded-[10px]' + ? 'w-[30px] h-[30px] rounded-surface' : 'w-[24px] h-[24px] rounded-md'; const filterAddRow = (
({ value={filterValue} onChange={onFilterChange} fullWidth - heightClass={renderMode === 'page' ? 'h-[30px]' : 'h-[24px]'} - paddingXClass={renderMode === 'page' ? 'px-[10px]' : 'px-[8px]'} - roundedClass={ - renderMode === 'page' ? 'rounded-[10px]' : 'rounded-md' - } + size={renderMode === 'page' ? 'lg' : 'sm'} />
) : null} @@ -296,7 +289,7 @@ export default function CommonListPickerPopup({ {/* 검색 — 리스트와 인접한 프라이머리 컨트롤 */}
{searchInput}
{/* 리스트 웰 — 리세스드 테이블. 빈 공간도 테이블의 빈 영역으로 읽힘 */} -
+
({ >
({
{listInner}
diff --git a/src/renderer/components/main/Modal/content/pickers/CounterAnimationPicker.tsx b/src/renderer/components/main/Modal/content/pickers/CounterAnimationPicker.tsx index 9fa0d1de..96b7d7e8 100644 --- a/src/renderer/components/main/Modal/content/pickers/CounterAnimationPicker.tsx +++ b/src/renderer/components/main/Modal/content/pickers/CounterAnimationPicker.tsx @@ -256,8 +256,6 @@ const CounterAnimationPicker = ({ pageTitle={pageTitle} onBack={onBack} onClose={handlePickerClose} - widthClass="w-[156px]" - estimatedWidth={164} estimatedHeight={276} searchQuery={searchQuery} onSearchQueryChange={setSearchQuery} diff --git a/src/renderer/components/main/Modal/content/pickers/FontPicker.tsx b/src/renderer/components/main/Modal/content/pickers/FontPicker.tsx index 31e92f70..f097e592 100644 --- a/src/renderer/components/main/Modal/content/pickers/FontPicker.tsx +++ b/src/renderer/components/main/Modal/content/pickers/FontPicker.tsx @@ -318,8 +318,6 @@ const FontPicker = ({ pageTitle={pageTitle} onBack={onBack} onClose={handlePickerClose} - widthClass="w-[156px]" - estimatedWidth={164} estimatedHeight={280} searchQuery={searchQuery} onSearchQueryChange={setSearchQuery} @@ -497,7 +495,7 @@ const FontPicker = ({ fallback={ setWebFontModal(null)}>
event.stopPropagation()} >

diff --git a/src/renderer/components/main/Modal/content/pickers/ImagePicker.tsx b/src/renderer/components/main/Modal/content/pickers/ImagePicker.tsx index 07135246..a1aa340b 100644 --- a/src/renderer/components/main/Modal/content/pickers/ImagePicker.tsx +++ b/src/renderer/components/main/Modal/content/pickers/ImagePicker.tsx @@ -187,7 +187,7 @@ const ImagePicker = ({ >

event.stopPropagation()} >
diff --git a/src/renderer/components/main/Modal/content/pickers/pickerRowClass.ts b/src/renderer/components/main/Modal/content/pickers/pickerRowClass.ts index ec360b75..4c0e1560 100644 --- a/src/renderer/components/main/Modal/content/pickers/pickerRowClass.ts +++ b/src/renderer/components/main/Modal/content/pickers/pickerRowClass.ts @@ -4,11 +4,11 @@ export type PickerRenderMode = 'popup' | 'page'; // 페이지 행: 인셋 웰(테이블) 내부의 필 행 — 30 컨트롤 스케일, 웰과 동심 라운딩 export const pickerRowClass = (mode: PickerRenderMode): string => mode === 'page' - ? 'w-full h-[30px] px-[8px] rounded-[6px] text-style-4 transition-colors flex items-center gap-[4px] group' + ? 'w-full h-[30px] px-[8px] rounded-md text-style-4 transition-colors flex items-center gap-[4px] group' : 'w-full h-[24px] px-[8px] rounded-md text-style-4 transition-colors flex items-center gap-[4px] group'; // 행 트레일링 ⋮ 버튼 — 페이지는 28px(30 행 스케일), 팝업은 18px 옵티컬 정렬 export const pickerMoreButtonClass = (mode: PickerRenderMode): string => mode === 'page' - ? 'w-[28px] h-[28px] rounded-[6px] transition-all flex items-center justify-center shrink-0' + ? 'w-[28px] h-[28px] rounded-md transition-all flex items-center justify-center shrink-0' : 'w-[18px] h-[18px] -mr-[8px] rounded-md transition-all flex items-center justify-center shrink-0'; diff --git a/src/renderer/components/main/Modal/content/settings/NoteSetting.tsx b/src/renderer/components/main/Modal/content/settings/NoteSetting.tsx index 1e63ab17..417a163f 100644 --- a/src/renderer/components/main/Modal/content/settings/NoteSetting.tsx +++ b/src/renderer/components/main/Modal/content/settings/NoteSetting.tsx @@ -458,7 +458,7 @@ const NoteSetting = ({ return (
e.stopPropagation()} > {title &&

{title}

} diff --git a/src/renderer/components/main/Modal/content/settings/ShortcutSettingsModal.tsx b/src/renderer/components/main/Modal/content/settings/ShortcutSettingsModal.tsx index 96c365b5..26ac5760 100644 --- a/src/renderer/components/main/Modal/content/settings/ShortcutSettingsModal.tsx +++ b/src/renderer/components/main/Modal/content/settings/ShortcutSettingsModal.tsx @@ -310,12 +310,6 @@ const ShortcutSettingsModal = ({ setListeningKey((prev) => (prev === key ? null : key)); }; - // const handleReset = () => { - // setError(null); - // setListeningKey(null); - // setDraft(defaults); - // }; - const handleSave = async () => { const validationError = validate(safeDraft); setError(validationError); @@ -338,7 +332,7 @@ const ShortcutSettingsModal = ({ }} >
event.stopPropagation()} onPointerDownCapture={(event) => { if (!isListening) return; @@ -498,16 +492,6 @@ const ShortcutSettingsModal = ({ > {t('shortcutSetting.cancel')} - {/* */}
diff --git a/src/renderer/components/main/Modal/content/settings/TabList.tsx b/src/renderer/components/main/Modal/content/settings/TabList.tsx index 00be5a81..c418e9ea 100644 --- a/src/renderer/components/main/Modal/content/settings/TabList.tsx +++ b/src/renderer/components/main/Modal/content/settings/TabList.tsx @@ -69,7 +69,7 @@ const TabList = ({ onClose: _onClose }: TabListProps) => { }; return ( -
+
{customTabs.length > 0 && ( <> {/* 섹션 헤더 */} @@ -87,41 +87,38 @@ const TabList = ({ onClose: _onClose }: TabListProps) => { .reverse() .map((tab) => { const isSelected = selectedKeyType === tab.id; + // 인터랙티브 요소 중첩 금지 — 행 래퍼는 비인터랙티브, + // 선택은 행 전체를 덮는 스트레치드 버튼, 삭제는 형제 button return ( - )} - +
); })}
@@ -133,7 +130,7 @@ const TabList = ({ onClose: _onClose }: TabListProps) => { {!maxReached && ( */} - ); -}; - -export default Button; diff --git a/src/renderer/components/main/common/Dropdown.tsx b/src/renderer/components/main/common/Dropdown.tsx index 35462e50..88d41270 100644 --- a/src/renderer/components/main/common/Dropdown.tsx +++ b/src/renderer/components/main/common/Dropdown.tsx @@ -20,12 +20,8 @@ interface DropdownProps { align?: 'left' | 'center' | 'right'; /** 트리거/메뉴 너비 고정용 Tailwind 클래스 (예: 'w-[160px]'). 길면 말줄임(...) 처리됨 */ widthClass?: string; - /** 트리거 높이 클래스 (기본 h-[24px]) */ - heightClass?: string; - /** 트리거 수평 패딩 클래스 (기본 px-[8px]) */ - paddingXClass?: string; - /** 트리거 라운딩 클래스 (기본 rounded-md) */ - roundedClass?: string; + /** 트리거 크기 — sm: 24px 크롬(기본), lg: 30px 크롬(패널 페이지) */ + size?: 'sm' | 'lg'; } interface MenuPosition { @@ -45,9 +41,7 @@ const Dropdown: React.FC = ({ iconTrigger, align = 'left', widthClass = '', - heightClass = 'h-[24px]', - paddingXClass = 'px-[8px]', - roundedClass = 'rounded-md', + size = 'sm', }) => { const [open, setOpen] = useState(false); const [menuPos, setMenuPos] = useState(null); @@ -138,7 +132,7 @@ const Dropdown: React.FC = ({
= ({ - ); -}; - -export default IconButton; diff --git a/src/renderer/components/main/common/Input.tsx b/src/renderer/components/main/common/Input.tsx deleted file mode 100644 index 30934d2c..00000000 --- a/src/renderer/components/main/common/Input.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React, { forwardRef } from 'react'; - -interface InputProps extends React.InputHTMLAttributes { - /** 텍스트 정렬 (좌표·수치 입력은 center 권장) */ - align?: 'left' | 'center'; -} - -const Input = forwardRef( - ({ align = 'left', className = '', ...rest }, ref) => { - return ( - - ); - }, -); - -Input.displayName = 'Input'; - -export default Input; diff --git a/src/renderer/components/main/common/Radio.tsx b/src/renderer/components/main/common/Radio.tsx deleted file mode 100644 index 61ecdee5..00000000 --- a/src/renderer/components/main/common/Radio.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; - -interface RadioProps { - value: string; - name: string; - checked: boolean; - onChange: (event: React.ChangeEvent) => void; - children: React.ReactNode; -} - -const Radio = ({ value, name, checked, onChange, children }: RadioProps) => { - return ( - - ); -}; - -export default Radio; diff --git a/src/renderer/components/main/common/TabSwitch.tsx b/src/renderer/components/main/common/TabSwitch.tsx index 4a636ef2..40ae6115 100644 --- a/src/renderer/components/main/common/TabSwitch.tsx +++ b/src/renderer/components/main/common/TabSwitch.tsx @@ -26,7 +26,7 @@ const TabSwitch = ({ return (
diff --git a/src/renderer/hooks/Grid/useGridKeyboard.ts b/src/renderer/hooks/Grid/useGridKeyboard.ts index 372b0410..76669926 100644 --- a/src/renderer/hooks/Grid/useGridKeyboard.ts +++ b/src/renderer/hooks/Grid/useGridKeyboard.ts @@ -161,8 +161,9 @@ export function useGridKeyboard({ return; } - // Escape 키로 선택 해제 + // Escape 키로 선택 해제 — 상위 레이어(메뉴·패널 페이지)가 소비했으면 양보 if (e.key === 'Escape') { + if (e.defaultPrevented) return; clearSelection(); return; } diff --git a/src/renderer/hooks/useLenis.ts b/src/renderer/hooks/useLenis.ts index bc02241b..ea304423 100644 --- a/src/renderer/hooks/useLenis.ts +++ b/src/renderer/hooks/useLenis.ts @@ -48,7 +48,6 @@ const easeOutExpo = (t: number): number => { export const useLenis = (options: UseLenisOptions = {}) => { const [wrapper, setWrapper] = useState(null); const wrapperRef = useRef(null); - const [scrollbarWidth, setScrollbarWidth] = useState(0); const lenisRef = useRef(null); const onScrollRef = useRef<(() => void) | undefined>(options.onScroll); @@ -74,14 +73,6 @@ export const useLenis = (options: UseLenisOptions = {}) => { useEffect(() => { if (!wrapper) return; - const computeScrollbarWidth = () => { - // offsetWidth는 스크롤바 포함, clientWidth는 스크롤바 제외 - const width = Math.max(0, wrapper.offsetWidth - wrapper.clientWidth); - setScrollbarWidth((prev) => (prev === width ? prev : width)); - }; - - computeScrollbarWidth(); - // 기존 Lenis 인스턴스 정리 if (lenisRef.current) { lenisRef.current.destroy(); @@ -120,21 +111,9 @@ export const useLenis = (options: UseLenisOptions = {}) => { }; rafId = requestAnimationFrame(raf); - // 스크롤바 너비 변화를 감지 (OS/시스템 설정에 따라 스크롤바가 레이아웃 폭을 차지할 수 있음) - let ro: ResizeObserver | null = null; - const onResize = () => computeScrollbarWidth(); - if (typeof ResizeObserver !== 'undefined') { - ro = new ResizeObserver(onResize); - ro.observe(wrapper); - } else { - window.addEventListener('resize', onResize); - } - // 클린업 return () => { cancelAnimationFrame(rafId); - if (ro) ro.disconnect(); - else window.removeEventListener('resize', onResize); lenis.off('scroll', handleLenisScroll); lenis.destroy(); lenisRef.current = null; @@ -146,7 +125,5 @@ export const useLenis = (options: UseLenisOptions = {}) => { /** 스크롤 컨테이너 DOM 요소 (state로 관리됨) */ wrapperElement: wrapper, lenisInstance: lenisRef, - /** wrapper의 실제 스크롤바 너비(px). overlay 스크롤바인 경우 0일 수 있음 */ - scrollbarWidth, }; }; diff --git a/src/renderer/styles/global.css b/src/renderer/styles/global.css index 530db8e5..4cfe9bc9 100644 --- a/src/renderer/styles/global.css +++ b/src/renderer/styles/global.css @@ -2,31 +2,6 @@ @tailwind components; @tailwind utilities; -/* 레거시 컴포넌트 클래스 — ui/ 프리미티브로 흡수 후 제거 예정 */ -@layer components { - .icon-btn { - @apply w-[24px] h-[24px] flex items-center justify-center hover:bg-surface-hover rounded-md transition-colors; - } - .input-field { - @apply text-center h-[23px] bg-inset rounded-md focus:shadow-focus-ring text-[11px] text-fg; - } - .btn-danger { - @apply bg-danger-muted rounded-md text-danger-fg; - } - .btn-danger:hover { - background: rgba(229, 72, 77, 0.2); - } - .btn-danger:active { - background: rgba(229, 72, 77, 0.26); - } - .btn-secondary { - @apply bg-surface hover:bg-surface-hover active:bg-surface-active rounded-md text-fg; - } - .btn-default { - @apply px-[10px] h-[23px] bg-surface hover:bg-surface-hover active:bg-surface-active rounded-md text-[11px]; - } -} - /* 기본 서체 — 가변 웨이트 셀프호스팅 */ @font-face { font-family: 'Pretendard Variable'; diff --git a/src/renderer/styles/main.css b/src/renderer/styles/main.css index 3cd8635c..63cab15d 100644 --- a/src/renderer/styles/main.css +++ b/src/renderer/styles/main.css @@ -16,70 +16,10 @@ body.dmn-custom-cursor * { /* ===== 스크롤바 ===== */ -.settings-scroll { - overscroll-behavior: contain; -} - -.settings-scroll::-webkit-scrollbar { - width: 6px; - background: transparent; -} - -.settings-scroll::-webkit-scrollbar-track { - background: transparent; -} - -.settings-scroll::-webkit-scrollbar-thumb { - background: transparent; - border-radius: 3px; -} - -.settings-scroll.show-scrollbar::-webkit-scrollbar-thumb, -.settings-scroll:hover::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.16); -} - -.settings-scroll.show-scrollbar::-webkit-scrollbar-thumb:hover, -.settings-scroll:hover::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.24); -} - -.settings-scroll.show-scrollbar::-webkit-scrollbar-thumb:active, -.settings-scroll:hover::-webkit-scrollbar-thumb:active { - background: rgba(255, 255, 255, 0.32); -} - .listpopup-scroll { overscroll-behavior: contain; } -.listpopup-scroll::-webkit-scrollbar { - width: 4px; - background: transparent; -} - -.listpopup-scroll::-webkit-scrollbar-track { - background: transparent; - margin: 4px 0; -} - -.listpopup-scroll::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.14); - border-radius: 4px; -} - -.listpopup-scroll:hover::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.2); -} - -.listpopup-scroll::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.26); -} - -.listpopup-scroll::-webkit-scrollbar-thumb:active { - background: rgba(255, 255, 255, 0.32); -} - .modal-content-scroll { overscroll-behavior: contain; /* 네이티브 동작 유지 */ @@ -88,20 +28,8 @@ body.dmn-custom-cursor * { contain: paint; } -.modal-content-scroll::-webkit-scrollbar { - width: 4px; - height: 4px; - background: transparent; -} - -.modal-content-scroll::-webkit-scrollbar-track { - background: transparent; - border-radius: 2px; -} - -.modal-content-scroll::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.14); - border-radius: 2px; +.settings-content-scroll { + overscroll-behavior: contain; } /* 스크롤바 전역 숨김 — 위치 단서는 스크롤 엣지 페이드 마스크가 대신함 */ @@ -113,42 +41,6 @@ body.dmn-custom-cursor * { display: none !important; } -.properties-panel-overlay-bar { - display: none !important; -} - -/* 설정 스크롤 - 호버 시에만 표시 */ -.settings-content-scroll { - overscroll-behavior: contain; -} - -.settings-content-scroll::-webkit-scrollbar { - width: 4px; - background: transparent; -} - -.settings-content-scroll::-webkit-scrollbar-track { - background: transparent; - border-radius: 2px; -} - -.settings-content-scroll::-webkit-scrollbar-thumb { - background: transparent; - border-radius: 2px; -} - -.settings-content-scroll.show-scrollbar::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.14); -} - -.settings-content-scroll.show-scrollbar::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.2); -} - -.settings-content-scroll.show-scrollbar::-webkit-scrollbar-thumb:active { - background: rgba(255, 255, 255, 0.26); -} - /* PropertiesPanel 오버레이 스크롤바 (레이아웃 수축 방지) */ .properties-panel-overlay-scroll { position: relative; @@ -209,42 +101,6 @@ body.dmn-custom-cursor * { } } -.properties-panel-overlay-viewport::-webkit-scrollbar { - width: 0; - height: 0; -} - -.properties-panel-overlay-bar { - position: absolute; - top: 0; - right: 0px; - bottom: 0; - width: 4px; - opacity: 0; - transition: opacity var(--ui-duration-fast) var(--ui-ease-out); - pointer-events: none; -} - -.properties-panel-overlay-scroll:hover .properties-panel-overlay-bar { - opacity: 1; -} - -.properties-panel-overlay-thumb { - position: absolute; - left: 0; - right: 0; - background: rgba(255, 255, 255, 0.14); - border-radius: 2px; -} - -.properties-panel-overlay-scroll:hover .properties-panel-overlay-thumb { - background: rgba(255, 255, 255, 0.2); -} - -.properties-panel-overlay-scroll:active .properties-panel-overlay-thumb { - background: rgba(255, 255, 255, 0.26); -} - /* ===== 등장 모션 ===== */ /* 화면 전환 (캔버스 ↔ 설정) */ @@ -285,8 +141,7 @@ body.dmn-custom-cursor * { inset: 0; display: flex; flex-direction: column; - transition: - opacity var(--ui-duration-page) var(--ui-ease-page), + transition: opacity var(--ui-duration-page) var(--ui-ease-page), transform var(--ui-duration-page) var(--ui-ease-page), filter var(--ui-duration-page) var(--ui-ease-page); } @@ -330,10 +185,12 @@ body.dmn-custom-cursor * { @keyframes modalScale { from { transform: scale(0.97); + opacity: 0; } to { transform: scale(1); + opacity: 1; } } @@ -341,14 +198,17 @@ body.dmn-custom-cursor * { animation: modalScale var(--ui-duration-base) var(--ui-ease-out) forwards; } -@keyframes modalFade { - to { - opacity: 1; +/* 틴트와 블러를 한 키프레임에서 보간 — 종료 상태는 클래스 자연값 */ +@keyframes modalScrim { + from { + background-color: transparent; + -webkit-backdrop-filter: blur(0px); + backdrop-filter: blur(0px); } } -.animate-modal-fade { - animation: modalFade var(--ui-duration-fast) var(--ui-ease-out) forwards; +.animate-modal-scrim { + animation: modalScrim var(--ui-duration-fast) var(--ui-ease-out) both; } /* ===== ColorPicker ===== */ @@ -525,16 +385,12 @@ body.dmn-custom-cursor * { @media (prefers-reduced-motion: reduce) { .tooltip-fade-in, .animate-modal-scale, - .animate-modal-fade, + .animate-modal-scrim, .view-enter, .view-fade { animation: none; } - .animate-modal-fade { - opacity: 1; - } - .dmn-panel-page { transition: none; } diff --git a/src/renderer/styles/tokens.css b/src/renderer/styles/tokens.css index 995881a2..0f730e5f 100644 --- a/src/renderer/styles/tokens.css +++ b/src/renderer/styles/tokens.css @@ -57,12 +57,19 @@ --ui-danger-hover: #ec5d62; --ui-danger-active: #d93b40; --ui-danger-muted: rgba(229, 72, 77, 0.14); + --ui-danger-muted-hover: rgba(229, 72, 77, 0.2); + --ui-danger-muted-active: rgba(229, 72, 77, 0.26); --ui-danger-fg: #ff9599; --ui-success: #3dbe7b; --ui-success-muted: rgba(61, 190, 123, 0.14); --ui-warning: #f0b429; --ui-warning-muted: rgba(240, 180, 41, 0.14); + /* 라운딩 — 3단: 표면 안 요소 6 < 1차 표면 10 < 모달 셸 14 */ + --ui-radius-inner: 6px; + --ui-radius-surface: 10px; + --ui-radius-modal: 14px; + /* 모션 */ --ui-duration-fast: 120ms; --ui-duration-base: 180ms; diff --git a/src/renderer/utils/plugin/pluginComponents.ts b/src/renderer/utils/plugin/pluginComponents.ts index 0c59677a..2a5931b4 100644 --- a/src/renderer/utils/plugin/pluginComponents.ts +++ b/src/renderer/utils/plugin/pluginComponents.ts @@ -278,7 +278,7 @@ export function createDropdown(options: DropdownOptions): string { const itemsHtml = items .map( (opt) => ` - - `; diff --git a/tailwind.config.js b/tailwind.config.js index 47c5d589..578de8a0 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -2,156 +2,160 @@ // 색·모션·그림자 값의 단일 소스는 src/renderer/styles/tokens.css module.exports = { - content: ["./src/**/*.{js,jsx,ts,tsx,html}"], + content: ['./src/**/*.{js,jsx,ts,tsx,html}'], theme: { extend: { + borderRadius: { + md: 'var(--ui-radius-inner)', + surface: 'var(--ui-radius-surface)', + modal: 'var(--ui-radius-modal)', + }, colors: { - app: "var(--ui-bg-app)", - panel: "var(--ui-bg-panel)", + app: 'var(--ui-bg-app)', + panel: 'var(--ui-bg-panel)', surface: { - DEFAULT: "var(--ui-bg-surface)", - hover: "var(--ui-bg-surface-hover)", - active: "var(--ui-bg-surface-active)", - }, - elevated: "var(--ui-bg-elevated)", - inset: "var(--ui-bg-inset)", - glass: "var(--ui-glass)", - "glass-heavy": "var(--ui-glass-heavy)", - "glass-dim": "var(--ui-glass-dim)", - "glass-panel": "var(--ui-glass-panel)", + DEFAULT: 'var(--ui-bg-surface)', + hover: 'var(--ui-bg-surface-hover)', + active: 'var(--ui-bg-surface-active)', + }, + elevated: 'var(--ui-bg-elevated)', + inset: 'var(--ui-bg-inset)', + glass: 'var(--ui-glass)', + 'glass-heavy': 'var(--ui-glass-heavy)', + 'glass-dim': 'var(--ui-glass-dim)', + 'glass-panel': 'var(--ui-glass-panel)', line: { - DEFAULT: "var(--ui-line)", - strong: "var(--ui-line-strong)", + DEFAULT: 'var(--ui-line)', + strong: 'var(--ui-line-strong)', }, fill: { - DEFAULT: "var(--ui-fill)", - hover: "var(--ui-fill-hover)", - active: "var(--ui-fill-active)", - faint: "var(--ui-fill-faint)", + DEFAULT: 'var(--ui-fill)', + hover: 'var(--ui-fill-hover)', + active: 'var(--ui-fill-active)', + faint: 'var(--ui-fill-faint)', }, fg: { - DEFAULT: "var(--ui-fg)", - muted: "var(--ui-fg-muted)", - faint: "var(--ui-fg-faint)", - disabled: "var(--ui-fg-disabled)", + DEFAULT: 'var(--ui-fg)', + muted: 'var(--ui-fg-muted)', + faint: 'var(--ui-fg-faint)', + disabled: 'var(--ui-fg-disabled)', }, accent: { - DEFAULT: "var(--ui-accent)", - hover: "var(--ui-accent-hover)", - active: "var(--ui-accent-active)", - muted: "var(--ui-accent-muted)", - fg: "var(--ui-accent-fg)", - deep: "var(--ui-accent-deep)", - "deep-hover": "var(--ui-accent-deep-hover)", - "deep-active": "var(--ui-accent-deep-active)", + DEFAULT: 'var(--ui-accent)', + hover: 'var(--ui-accent-hover)', + active: 'var(--ui-accent-active)', + muted: 'var(--ui-accent-muted)', + fg: 'var(--ui-accent-fg)', + deep: 'var(--ui-accent-deep)', + 'deep-hover': 'var(--ui-accent-deep-hover)', + 'deep-active': 'var(--ui-accent-deep-active)', }, danger: { - DEFAULT: "var(--ui-danger)", - hover: "var(--ui-danger-hover)", - active: "var(--ui-danger-active)", - muted: "var(--ui-danger-muted)", - fg: "var(--ui-danger-fg)", + DEFAULT: 'var(--ui-danger)', + hover: 'var(--ui-danger-hover)', + active: 'var(--ui-danger-active)', + muted: 'var(--ui-danger-muted)', + 'muted-hover': 'var(--ui-danger-muted-hover)', + 'muted-active': 'var(--ui-danger-muted-active)', + fg: 'var(--ui-danger-fg)', }, success: { - DEFAULT: "var(--ui-success)", - muted: "var(--ui-success-muted)", + DEFAULT: 'var(--ui-success)', + muted: 'var(--ui-success-muted)', }, warning: { - DEFAULT: "var(--ui-warning)", - muted: "var(--ui-warning-muted)", + DEFAULT: 'var(--ui-warning)', + muted: 'var(--ui-warning-muted)', }, }, fontFamily: { sans: [ - "Pretendard Variable", - "Pretendard", - "-apple-system", - "BlinkMacSystemFont", - "system-ui", - "Segoe UI", - "sans-serif", + 'Pretendard Variable', + 'Pretendard', + '-apple-system', + 'BlinkMacSystemFont', + 'system-ui', + 'Segoe UI', + 'sans-serif', ], }, transitionDuration: { - fast: "120ms", - base: "180ms", - slow: "240ms", + fast: '120ms', + base: '180ms', + slow: '240ms', }, transitionTimingFunction: { - "out-expo": "cubic-bezier(0.16, 1, 0.3, 1)", - "in-out-smooth": "cubic-bezier(0.65, 0, 0.35, 1)", + 'out-expo': 'cubic-bezier(0.16, 1, 0.3, 1)', + 'in-out-smooth': 'cubic-bezier(0.65, 0, 0.35, 1)', }, boxShadow: { - "elevation-1": "var(--ui-shadow-1)", - "elevation-2": "var(--ui-shadow-2)", - "elevation-3": "var(--ui-shadow-3)", - "elevation-chrome": "var(--ui-shadow-chrome)", - "elevation-panel": "var(--ui-shadow-panel)", - "focus-ring": "var(--ui-focus-ring)", + 'elevation-1': 'var(--ui-shadow-1)', + 'elevation-2': 'var(--ui-shadow-2)', + 'elevation-3': 'var(--ui-shadow-3)', + 'elevation-chrome': 'var(--ui-shadow-chrome)', + 'elevation-panel': 'var(--ui-shadow-panel)', + 'focus-ring': 'var(--ui-focus-ring)', }, }, }, plugins: [ function ({ addUtilities }) { addUtilities({ - ".all-unset": { - all: "unset", - }, // 타이포그래피 스케일 — 위계는 크기+웨이트+행간 세트로만 표현 - ".text-caption": { - fontSize: "11px", - lineHeight: "16px", - letterSpacing: "0.01em", - fontWeight: "500", - }, - ".text-body": { - fontSize: "12px", - lineHeight: "18px", - letterSpacing: "0", - fontWeight: "500", - }, - ".text-label": { - fontSize: "13px", - lineHeight: "18px", - letterSpacing: "0", - fontWeight: "500", - }, - ".text-title": { - fontSize: "14px", - lineHeight: "20px", - letterSpacing: "-0.01em", - fontWeight: "600", - }, - ".text-heading": { - fontSize: "16px", - lineHeight: "22px", - letterSpacing: "-0.014em", - fontWeight: "700", + '.text-caption': { + fontSize: '11px', + lineHeight: '16px', + letterSpacing: '0.01em', + fontWeight: '500', + }, + '.text-body': { + fontSize: '12px', + lineHeight: '18px', + letterSpacing: '0', + fontWeight: '500', + }, + '.text-label': { + fontSize: '13px', + lineHeight: '18px', + letterSpacing: '0', + fontWeight: '500', + }, + '.text-title': { + fontSize: '14px', + lineHeight: '20px', + letterSpacing: '-0.01em', + fontWeight: '600', + }, + '.text-heading': { + fontSize: '16px', + lineHeight: '22px', + letterSpacing: '-0.014em', + fontWeight: '700', }, // 레거시 별칭 — 마이그레이션 완료 후 제거 - ".text-style-1": { - fontSize: "12px", - lineHeight: "18px", - letterSpacing: "0", - fontWeight: "500", - }, - ".text-style-2": { - fontSize: "13px", - lineHeight: "18px", - letterSpacing: "0", - fontWeight: "500", - }, - ".text-style-3": { - fontSize: "14px", - lineHeight: "20px", - letterSpacing: "0", - fontWeight: "500", - }, - ".text-style-4": { - fontSize: "14px", - lineHeight: "20px", - letterSpacing: "0", - fontWeight: "500", + '.text-style-1': { + fontSize: '12px', + lineHeight: '18px', + letterSpacing: '0', + fontWeight: '500', + }, + '.text-style-2': { + fontSize: '13px', + lineHeight: '18px', + letterSpacing: '0', + fontWeight: '500', + }, + '.text-style-3': { + fontSize: '14px', + lineHeight: '20px', + letterSpacing: '0', + fontWeight: '500', + }, + '.text-style-4': { + fontSize: '14px', + lineHeight: '20px', + letterSpacing: '0', + fontWeight: '500', }, }); }, From 255bcdc691f9d277dfb7e16e937637cc9ca00316 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=97=B0=EC=9A=B0?= Date: Sat, 11 Jul 2026 20:43:16 +0900 Subject: [PATCH 02/17] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=B4=EB=93=9C=20?= =?UTF-8?q?=EC=88=A8=EA=B9=80=20=EB=B3=B5=EA=B5=AC=20=EB=B0=8F=20=ED=94=BC?= =?UTF-8?q?=EC=BB=A4=C2=B7=EB=AA=A8=EB=8B=AC=20=EC=83=81=ED=98=B8=EC=9E=91?= =?UTF-8?q?=EC=9A=A9=20=EC=A0=95=EB=B9=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사운드 숨김: - SoundLibraryEntry.hidden 신설, 구 enabled 키는 로드 시 raw JSON 단계에서 hidden으로 역변환 후 재영속 (멱등, 마이그레이션 테스트 4종) - sound_set_hidden 커맨드 + 구 계약 호환 sound_set_enabled 별칭 - 피커 행 우클릭/⋮ 메뉴에 숨기기·숨김 해제 (내장 사운드 포함), 필터에 '숨긴 사운드' 추가 — 기본 뷰에서는 숨김 제외 - 재생 경로 미연결: 할당된 키음은 숨겨도 계속 재생 - i18n 5로케일, dmn.sound API 문서 신규 (en/ko) 피커 페이지 전용화: - CommonListPickerPopup → CommonListPickerPage 개명, 죽은 플로팅 팝업 분기·위치 계산·no-op prop 제거 (피커 3종, 트리거 8곳) - ⋮ 버튼 -mr-8 정렬 사각형으로 아이콘 시각 인셋을 텍스트와 8px 대칭, 카운터 애니메이션 ⋮ 호버를 색상 전환으로 통일 모달 스크롤 그림자 현대화: - Alert·UnifiedKeySetting·ManagerModalLayout의 JS 그림자 계산을 dmn-scroll-fade 마스크로 교체, scrollShadow 유틸 삭제 - 높이 애니메이션은 콘텐츠 ResizeObserver만 남기고 보존 레이어·토큰: - z-계층 규칙 편입: 서브메뉴 10001→60, !z-10000 오버라이드 제거 (본체 40) - --ui-selection* 토큰 신설, 캔버스 선택 크롬(핸들·마퀴·가이드)의 블루 리터럴 11곳을 바이올렛으로 교체 - Escape 소유권 완성: 드롭다운·서브메뉴·플로팅 피커가 각자 소비해 서브메뉴 → 메뉴 → 페이지 → 선택 해제 순으로 한 겹씩 닫힘 - 서브메뉴 위치 추정치(160) 대신 실측 배치, 패널 헤더 aria-label 보강, useLenis 미사용 onScroll 제거, clippy 경고 전면 해소 --- docs/content/en/api-reference/_meta.js | 1 + docs/content/en/api-reference/sound/page.mdx | 102 ++++++ docs/content/ko/api-reference/_meta.js | 1 + docs/content/ko/api-reference/sound/page.mdx | 102 ++++++ src-tauri/build.rs | 5 +- src-tauri/gen/schemas/acl-manifests.json | 2 +- src-tauri/permissions/dmnote-allow-all.json | 2 + src-tauri/src/audio/engine.rs | 2 +- src-tauri/src/commands/keys/sound.rs | 72 ++++ src-tauri/src/cursor.rs | 2 +- src-tauri/src/keyboard/daemon/mod.rs | 2 +- src-tauri/src/main.rs | 6 +- src-tauri/src/models/mod.rs | 5 +- src-tauri/src/state/app_state.rs | 7 +- src-tauri/src/state/builtin_sounds.rs | 6 + src-tauri/src/state/migration.rs | 144 +++++++- src/renderer/api/modules/resourceApi.ts | 11 + .../components/main/Grid/PropertiesPanel.tsx | 1 + .../PluginSettingsPanelView.tsx | 1 + .../batch/BatchCounterTabContent.tsx | 12 +- .../batch/BatchSelectionPanel.tsx | 12 + .../batch/BatchStyleTabContent.tsx | 10 - .../Grid/PropertiesPanel/layer/LayerPanel.tsx | 4 + .../PropertiesPanel/layer/LayerTabContent.tsx | 1 - .../single/CounterTabContent.tsx | 10 - .../single/SingleSelectionPanel.tsx | 10 + .../single/StyleTabContent.tsx | 10 - .../components/main/Grid/core/Grid.tsx | 2 +- .../main/Grid/handles/GroupResizeHandles.tsx | 6 +- .../main/Grid/handles/ResizeHandles.tsx | 4 +- .../Grid/overlays/MarqueeSelectionOverlay.tsx | 4 +- .../main/Grid/overlays/SmartGuidesOverlay.tsx | 8 +- .../components/main/Modal/FloatingPopup.tsx | 25 +- .../components/main/Modal/ListPopup.tsx | 80 ++-- .../main/Modal/ManagerModalLayout.tsx | 100 ++--- .../main/Modal/content/dialogs/Alert.tsx | 62 +--- .../content/dialogs/UnifiedKeySetting.tsx | 114 ++---- .../content/pickers/CommonListPickerPage.tsx | 188 ++++++++++ .../content/pickers/CommonListPickerPopup.tsx | 343 ------------------ .../pickers/CounterAnimationPicker.tsx | 39 +- .../main/Modal/content/pickers/FontPicker.tsx | 36 +- .../Modal/content/pickers/SoundPicker.tsx | 158 ++++---- .../Modal/content/pickers/pickerRowClass.ts | 20 +- .../components/main/common/Dropdown.tsx | 12 + .../components/shared/PluginElement.tsx | 1 - src/renderer/hooks/useLenis.ts | 18 - src/renderer/locales/en.json | 4 + src/renderer/locales/ko.json | 4 + src/renderer/locales/ru.json | 4 + src/renderer/locales/zh-Hant.json | 4 + src/renderer/locales/zh-cn.json | 4 + src/renderer/styles/tokens.css | 7 + src/renderer/utils/grid/scrollShadow.ts | 90 ----- src/types/plugin/api.ts | 23 ++ 54 files changed, 951 insertions(+), 952 deletions(-) create mode 100644 docs/content/en/api-reference/sound/page.mdx create mode 100644 docs/content/ko/api-reference/sound/page.mdx create mode 100644 src/renderer/components/main/Modal/content/pickers/CommonListPickerPage.tsx delete mode 100644 src/renderer/components/main/Modal/content/pickers/CommonListPickerPopup.tsx delete mode 100644 src/renderer/utils/grid/scrollShadow.ts diff --git a/docs/content/en/api-reference/_meta.js b/docs/content/en/api-reference/_meta.js index 944a3cb3..7b2712b2 100644 --- a/docs/content/en/api-reference/_meta.js +++ b/docs/content/en/api-reference/_meta.js @@ -6,6 +6,7 @@ export default { settings: "Settings", overlay: "Overlay", "css-js": "CSS/JS", + sound: "Sound", presets: "Presets", i18n: "i18n", plugin: "Plugin", diff --git a/docs/content/en/api-reference/sound/page.mdx b/docs/content/en/api-reference/sound/page.mdx new file mode 100644 index 00000000..e1bbf251 --- /dev/null +++ b/docs/content/en/api-reference/sound/page.mdx @@ -0,0 +1,102 @@ +--- +title: Sound API +description: dmn.sound API reference for the key sound library +--- + +# Sound API + +Manage the key sound library — the pool of audio files that key elements can +play on press. Sounds live in the app data `sounds` directory and include both +user-imported files (`local`) and bundled ones (`builtin`). + +## Types + +```typescript +type SoundListItem = { + soundPath: string; // absolute path — also the stable identifier + fileName: string; + sizeBytes: number; + modifiedAtMs?: number; + hidden: boolean; // hidden from picker lists — playback is unaffected + source: 'local' | 'builtin'; + originalPath?: string; // pre-trim original (present for edited sounds) + trimStartRatio?: number; + trimEndRatio?: number; + displayName?: string; +}; +``` + +## Library + +### `dmn.sound.list(): Promise` + +Returns every sound in the library, **including hidden ones** — filtering by +`hidden` is up to the caller (the built-in picker hides them from its default +views and shows them under a "Hidden Sounds" filter). + +```typescript +const sounds = await dmn.sound.list(); +const visible = sounds.filter((s) => !s.hidden); +``` + +### `dmn.sound.load(): Promise` + +Opens a file dialog, copies the chosen audio file into the library, and +returns its `soundPath`. + +### `dmn.sound.rename(soundPath, displayName): Promise` + +Sets the display name shown in pickers. The file itself is not renamed. + +### `dmn.sound.remove(soundPath): Promise` + +Deletes the file and unassigns the sound from every element using it. + +## Visibility + +### `dmn.sound.setHidden(soundPath, hidden): Promise` + +Hides a sound from (or restores it to) picker lists. Hiding is list-only +housekeeping: **keys that already use the sound keep playing it**. Builtin +sounds can be hidden too. + +```typescript +await dmn.sound.setHidden(sound.soundPath, true); // hide +await dmn.sound.setHidden(sound.soundPath, false); // unhide +``` + +```typescript +type SoundSetHiddenResult = { + success: boolean; + soundPath: string; + hidden: boolean; +}; +``` + +### `dmn.sound.setEnabled(soundPath, enabled): Promise` + + + Deprecated — inverse alias kept for backward compatibility. `enabled` is + simply `!hidden`; use `setHidden` instead. + + +## Editing + +### `dmn.sound.saveProcessedWav(wavBase64, fileName?, originalBase64?, originalExtension?, trimStartRatio?, trimEndRatio?): Promise` + +Saves a processed (trimmed) WAV into the library, optionally alongside the +original for later re-editing. + +### `dmn.sound.loadOriginal(soundPath): Promise` + +Returns the stored original audio (base64) of an edited sound. + +### `dmn.sound.updateProcessedWav(soundPath, wavBase64, trimStartRatio?, trimEndRatio?, displayName?): Promise` + +Overwrites an edited sound's processed WAV in place. + +## Diagnostics + +### `dmn.sound.setLatencyLogging(enabled): Promise` + +Toggles key-sound latency logging in the audio engine. diff --git a/docs/content/ko/api-reference/_meta.js b/docs/content/ko/api-reference/_meta.js index f960cdba..bcffa889 100644 --- a/docs/content/ko/api-reference/_meta.js +++ b/docs/content/ko/api-reference/_meta.js @@ -6,6 +6,7 @@ export default { settings: "설정 (settings)", overlay: "오버레이 (overlay)", "css-js": "CSS/JS", + sound: "사운드 (sound)", presets: "프리셋 (presets)", i18n: "다국어 (i18n)", plugin: "플러그인 (plugin)", diff --git a/docs/content/ko/api-reference/sound/page.mdx b/docs/content/ko/api-reference/sound/page.mdx new file mode 100644 index 00000000..999dd295 --- /dev/null +++ b/docs/content/ko/api-reference/sound/page.mdx @@ -0,0 +1,102 @@ +--- +title: 사운드 API +description: 키음 라이브러리를 위한 dmn.sound API 레퍼런스 +--- + +# 사운드 API + +키음 라이브러리 — 키 요소가 눌릴 때 재생할 수 있는 오디오 파일 풀 — 를 +관리합니다. 사운드는 앱 데이터의 `sounds` 디렉토리에 저장되며, 사용자가 +가져온 파일(`local`)과 기본 내장 파일(`builtin`)로 나뉩니다. + +## 타입 + +```typescript +type SoundListItem = { + soundPath: string; // 절대 경로 — 안정적인 식별자로도 사용 + fileName: string; + sizeBytes: number; + modifiedAtMs?: number; + hidden: boolean; // 피커 목록에서 숨김 — 재생에는 영향 없음 + source: 'local' | 'builtin'; + originalPath?: string; // 트림 전 원본 (편집된 사운드에 존재) + trimStartRatio?: number; + trimEndRatio?: number; + displayName?: string; +}; +``` + +## 라이브러리 + +### `dmn.sound.list(): Promise` + +라이브러리의 모든 사운드를 **숨긴 항목까지 포함해** 반환합니다. `hidden` +기준 필터링은 호출자의 몫입니다 (내장 피커는 기본 뷰에서 숨기고 "숨긴 +사운드" 필터에서 보여줍니다). + +```typescript +const sounds = await dmn.sound.list(); +const visible = sounds.filter((s) => !s.hidden); +``` + +### `dmn.sound.load(): Promise` + +파일 대화상자를 열어 선택한 오디오 파일을 라이브러리로 복사하고 +`soundPath`를 반환합니다. + +### `dmn.sound.rename(soundPath, displayName): Promise` + +피커에 표시되는 이름을 설정합니다. 파일 이름 자체는 바뀌지 않습니다. + +### `dmn.sound.remove(soundPath): Promise` + +파일을 삭제하고, 이 사운드를 사용하는 모든 요소에서 할당을 해제합니다. + +## 표시 여부 + +### `dmn.sound.setHidden(soundPath, hidden): Promise` + +사운드를 피커 목록에서 숨기거나 다시 표시합니다. 숨김은 목록 정리용 +기능일 뿐이라 **이미 이 사운드를 쓰는 키는 계속 재생됩니다**. 내장 +사운드도 숨길 수 있습니다. + +```typescript +await dmn.sound.setHidden(sound.soundPath, true); // 숨기기 +await dmn.sound.setHidden(sound.soundPath, false); // 숨김 해제 +``` + +```typescript +type SoundSetHiddenResult = { + success: boolean; + soundPath: string; + hidden: boolean; +}; +``` + +### `dmn.sound.setEnabled(soundPath, enabled): Promise` + + + Deprecated — 하위 호환용 역논리 별칭입니다. `enabled`는 단순히 + `!hidden`이며, `setHidden`을 사용하세요. + + +## 편집 + +### `dmn.sound.saveProcessedWav(wavBase64, fileName?, originalBase64?, originalExtension?, trimStartRatio?, trimEndRatio?): Promise` + +가공(트림)된 WAV를 라이브러리에 저장합니다. 나중에 다시 편집할 수 있도록 +원본을 함께 저장할 수 있습니다. + +### `dmn.sound.loadOriginal(soundPath): Promise` + +편집된 사운드의 저장된 원본 오디오(base64)를 반환합니다. + +### `dmn.sound.updateProcessedWav(soundPath, wavBase64, trimStartRatio?, trimEndRatio?, displayName?): Promise` + +편집된 사운드의 가공 WAV를 제자리에서 덮어씁니다. + +## 진단 + +### `dmn.sound.setLatencyLogging(enabled): Promise` + +오디오 엔진의 키음 지연 로깅을 켜거나 끕니다. diff --git a/src-tauri/build.rs b/src-tauri/build.rs index 2e1234f7..09b6045f 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -112,10 +112,8 @@ fn generate_permissions() { fn extract_fn_name(line: &str) -> Option { let rest = if let Some(r) = line.strip_prefix("pub async fn ") { r - } else if let Some(r) = line.strip_prefix("pub fn ") { - r } else { - return None; + line.strip_prefix("pub fn ")? }; rest.split('(').next().map(|s| s.trim().to_string()) } @@ -210,7 +208,6 @@ fn maybe_build_macos_dock_helper() { if let Err(err) = fs::copy(&source_icon, &helper_icon) { println!("cargo:warning=failed to copy helper icon: {err}"); - return; } } diff --git a/src-tauri/gen/schemas/acl-manifests.json b/src-tauri/gen/schemas/acl-manifests.json index d1747390..3e8127dc 100644 --- a/src-tauri/gen/schemas/acl-manifests.json +++ b/src-tauri/gen/schemas/acl-manifests.json @@ -1 +1 @@ -{"__app-acl__":{"default_permission":null,"permissions":{"dmnote-allow-all":{"identifier":"dmnote-allow-all","description":"Full DM Note command access for renderer","commands":{"allow":["app_auto_update","app_bootstrap","app_open_external","app_quit","app_restart","counter_animation_create","counter_animation_delete","counter_animation_list","counter_animation_update","css_get","css_get_use","css_load","css_reset","css_set_content","css_tab_clear","css_tab_get","css_tab_get_all","css_tab_load","css_tab_set","css_tab_toggle","css_toggle","custom_tabs_create","custom_tabs_delete","custom_tabs_list","custom_tabs_restore","custom_tabs_select","font_load","get_cursor_settings","graph_positions_get","graph_positions_update","image_load","js_get","js_get_use","js_load","js_reload","js_remove_plugin","js_reset","js_set_content","js_set_plugin_enabled","js_toggle","key_sound_get_output_state","key_sound_get_status","key_sound_list_output_devices","key_sound_load_soundpack","key_sound_set_enabled","key_sound_set_latency_logging","key_sound_set_output_backend","key_sound_set_volume","key_sound_unload_soundpack","keys_get","keys_get_counters","keys_reset_all","keys_reset_counters","keys_reset_counters_mode","keys_reset_mode","keys_reset_single_counter","keys_set_counters","keys_set_mode","keys_update","knob_positions_get","knob_positions_update","layer_groups_get","layer_groups_update","note_tab_clear","note_tab_get","note_tab_get_all","note_tab_set","obs_regenerate_token","obs_start","obs_status","obs_stop","overlay_get","overlay_resize","overlay_set_anchor","overlay_set_lock","overlay_set_visible","plugin_bridge_send","plugin_bridge_send_to","plugin_storage_clear","plugin_storage_clear_by_prefix","plugin_storage_get","plugin_storage_has_data","plugin_storage_keys","plugin_storage_remove","plugin_storage_set","positions_get","positions_update","preset_load","preset_load_tab","preset_save","preset_save_tab","raw_input_subscribe","raw_input_unsubscribe","settings_get","settings_update","sound_delete","sound_list","sound_load","sound_load_original","sound_rename","sound_save_processed_wav","sound_update_processed_wav","stat_positions_get","stat_positions_update","window_close","window_minimize","window_open_devtools_all","window_show_main"],"deny":[]}}},"permission_sets":{},"global_scope_schema":null},"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_data_store"]}},"deny-remove-listener":{"identifier":"deny-remove-listener","description":"Denies the remove_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_listener"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-set-dock-visibility":{"identifier":"deny-set-dock-visibility","description":"Denies the set_dock_visibility command without any pre-configured scope.","commands":{"allow":[],"deny":["set_dock_visibility"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-auto-resize":{"identifier":"allow-set-webview-auto-resize","description":"Enables the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":["set_webview_auto_resize"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-auto-resize":{"identifier":"deny-set-webview-auto-resize","description":"Denies the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_auto_resize"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-is-always-on-top","allow-internal-toggle-maximize"]},"permissions":{"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-always-on-top":{"identifier":"allow-is-always-on-top","description":"Enables the is_always_on_top command without any pre-configured scope.","commands":{"allow":["is_always_on_top"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-focusable":{"identifier":"allow-set-focusable","description":"Enables the set_focusable command without any pre-configured scope.","commands":{"allow":["set_focusable"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-simple-fullscreen":{"identifier":"allow-set-simple-fullscreen","description":"Enables the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":["set_simple_fullscreen"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-always-on-top":{"identifier":"deny-is-always-on-top","description":"Denies the is_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["is_always_on_top"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-focusable":{"identifier":"deny-set-focusable","description":"Denies the set_focusable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focusable"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-simple-fullscreen":{"identifier":"deny-set-simple-fullscreen","description":"Denies the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_simple_fullscreen"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null}} \ No newline at end of file +{"__app-acl__":{"default_permission":null,"permissions":{"dmnote-allow-all":{"identifier":"dmnote-allow-all","description":"Full DM Note command access for renderer","commands":{"allow":["app_auto_update","app_bootstrap","app_open_external","app_quit","app_restart","counter_animation_create","counter_animation_delete","counter_animation_list","counter_animation_update","css_get","css_get_use","css_load","css_reset","css_set_content","css_tab_clear","css_tab_get","css_tab_get_all","css_tab_load","css_tab_set","css_tab_toggle","css_toggle","custom_tabs_create","custom_tabs_delete","custom_tabs_list","custom_tabs_restore","custom_tabs_select","font_load","get_cursor_settings","graph_positions_get","graph_positions_update","image_load","js_get","js_get_use","js_load","js_reload","js_remove_plugin","js_reset","js_set_content","js_set_plugin_enabled","js_toggle","key_sound_get_output_state","key_sound_get_status","key_sound_list_output_devices","key_sound_load_soundpack","key_sound_set_enabled","key_sound_set_latency_logging","key_sound_set_output_backend","key_sound_set_volume","key_sound_unload_soundpack","keys_get","keys_get_counters","keys_reset_all","keys_reset_counters","keys_reset_counters_mode","keys_reset_mode","keys_reset_single_counter","keys_set_counters","keys_set_mode","keys_update","knob_positions_get","knob_positions_update","layer_groups_get","layer_groups_update","note_tab_clear","note_tab_get","note_tab_get_all","note_tab_set","obs_regenerate_token","obs_start","obs_status","obs_stop","overlay_get","overlay_resize","overlay_set_anchor","overlay_set_lock","overlay_set_visible","plugin_bridge_send","plugin_bridge_send_to","plugin_storage_clear","plugin_storage_clear_by_prefix","plugin_storage_get","plugin_storage_has_data","plugin_storage_keys","plugin_storage_remove","plugin_storage_set","positions_get","positions_update","preset_load","preset_load_tab","preset_save","preset_save_tab","raw_input_subscribe","raw_input_unsubscribe","settings_get","settings_update","sound_delete","sound_list","sound_load","sound_load_original","sound_rename","sound_save_processed_wav","sound_set_enabled","sound_set_hidden","sound_update_processed_wav","stat_positions_get","stat_positions_update","window_close","window_minimize","window_open_devtools_all","window_show_main"],"deny":[]}}},"permission_sets":{},"global_scope_schema":null},"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_data_store"]}},"deny-remove-listener":{"identifier":"deny-remove-listener","description":"Denies the remove_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_listener"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-set-dock-visibility":{"identifier":"deny-set-dock-visibility","description":"Denies the set_dock_visibility command without any pre-configured scope.","commands":{"allow":[],"deny":["set_dock_visibility"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-auto-resize":{"identifier":"allow-set-webview-auto-resize","description":"Enables the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":["set_webview_auto_resize"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-auto-resize":{"identifier":"deny-set-webview-auto-resize","description":"Denies the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_auto_resize"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-is-always-on-top","allow-internal-toggle-maximize"]},"permissions":{"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-always-on-top":{"identifier":"allow-is-always-on-top","description":"Enables the is_always_on_top command without any pre-configured scope.","commands":{"allow":["is_always_on_top"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-focusable":{"identifier":"allow-set-focusable","description":"Enables the set_focusable command without any pre-configured scope.","commands":{"allow":["set_focusable"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-simple-fullscreen":{"identifier":"allow-set-simple-fullscreen","description":"Enables the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":["set_simple_fullscreen"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-always-on-top":{"identifier":"deny-is-always-on-top","description":"Denies the is_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["is_always_on_top"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-focusable":{"identifier":"deny-set-focusable","description":"Denies the set_focusable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focusable"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-simple-fullscreen":{"identifier":"deny-set-simple-fullscreen","description":"Denies the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_simple_fullscreen"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null}} \ No newline at end of file diff --git a/src-tauri/permissions/dmnote-allow-all.json b/src-tauri/permissions/dmnote-allow-all.json index b9e66679..82e07c61 100644 --- a/src-tauri/permissions/dmnote-allow-all.json +++ b/src-tauri/permissions/dmnote-allow-all.json @@ -107,6 +107,8 @@ "sound_load_original", "sound_rename", "sound_save_processed_wav", + "sound_set_enabled", + "sound_set_hidden", "sound_update_processed_wav", "stat_positions_get", "stat_positions_update", diff --git a/src-tauri/src/audio/engine.rs b/src-tauri/src/audio/engine.rs index 5f165024..f3ef8f29 100644 --- a/src-tauri/src/audio/engine.rs +++ b/src-tauri/src/audio/engine.rs @@ -130,7 +130,7 @@ impl LatencySummary { } fn should_emit_summary(&self) -> bool { - self.samples > 0 && self.samples % LATENCY_SUMMARY_INTERVAL == 0 + self.samples > 0 && self.samples.is_multiple_of(LATENCY_SUMMARY_INTERVAL) } fn emit_summary(&self) { diff --git a/src-tauri/src/commands/keys/sound.rs b/src-tauri/src/commands/keys/sound.rs index 85aa832a..f41544ea 100644 --- a/src-tauri/src/commands/keys/sound.rs +++ b/src-tauri/src/commands/keys/sound.rs @@ -35,6 +35,7 @@ pub struct SoundListItem { pub size_bytes: u64, #[serde(skip_serializing_if = "Option::is_none")] pub modified_at_ms: Option, + pub hidden: bool, pub source: SoundSource, #[serde(skip_serializing_if = "Option::is_none")] pub original_path: Option, @@ -80,6 +81,22 @@ pub struct SoundRenameResponse { pub display_name: String, } +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SoundSetHiddenResponse { + pub success: bool, + pub sound_path: String, + pub hidden: bool, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SoundSetEnabledResponse { + pub success: bool, + pub sound_path: String, + pub enabled: bool, +} + /// 로컬 사운드 파일을 선택하고 appData/sounds 디렉토리로 복사한 뒤 경로 반환 #[tauri::command] pub fn sound_load( @@ -187,6 +204,7 @@ pub fn sound_list( file_name, size_bytes: metadata.len(), modified_at_ms, + hidden: entry_meta.hidden, source: entry_meta.source, original_path: entry_meta.original_path, trim_start_ratio: entry_meta.trim_start_ratio, @@ -230,6 +248,59 @@ pub fn sound_list( Ok(items) } +#[tauri::command] +pub fn sound_set_hidden( + app: tauri::AppHandle, + state: State<'_, AppState>, + sound_path: String, + hidden: bool, +) -> CmdResult { + let path_key = set_sound_hidden(&app, state.inner(), &sound_path, hidden)?; + Ok(SoundSetHiddenResponse { + success: true, + sound_path: path_key, + hidden, + }) +} + +#[tauri::command] +pub fn sound_set_enabled( + app: tauri::AppHandle, + state: State<'_, AppState>, + sound_path: String, + enabled: bool, +) -> CmdResult { + let path_key = set_sound_hidden(&app, state.inner(), &sound_path, !enabled)?; + Ok(SoundSetEnabledResponse { + success: true, + sound_path: path_key, + enabled, + }) +} + +fn set_sound_hidden( + app: &tauri::AppHandle, + state: &AppState, + sound_path: &str, + hidden: bool, +) -> CmdResult { + let sounds_dir = ensure_sounds_dir(app)?; + let validated_path = validate_sound_path(&sounds_dir, sound_path)?; + if !validated_path.exists() { + return Err(CommandError::msg("대상 사운드 파일이 존재하지 않습니다.")); + } + + let path_key = normalize_path_string(&validated_path); + state.store.update(|store| { + store + .sound_library + .entry(path_key.clone()) + .or_default() + .hidden = hidden; + })?; + Ok(path_key) +} + #[tauri::command] pub fn sound_rename( app: tauri::AppHandle, @@ -459,6 +530,7 @@ pub fn sound_save_processed_wav( s.sound_library.insert( dest_path_str.clone(), SoundLibraryEntry { + hidden: false, source: SoundSource::Local, original_path: original_rel_path.clone(), trim_start_ratio: request.trim_start_ratio, diff --git a/src-tauri/src/cursor.rs b/src-tauri/src/cursor.rs index bc369ba7..8c9ca692 100644 --- a/src-tauri/src/cursor.rs +++ b/src-tauri/src/cursor.rs @@ -85,7 +85,7 @@ pub fn get_macos_cursor_settings() -> MacOSCursorSettings { .or_else(|| read_double_from_user_defaults(suite_defaults, "mouseDriverCursorSize")) .or_else(|| read_double_from_prefs(prefs, "mouseDriverCursorSize")); if let Some(size) = size { - if size >= 1.0 && size <= 4.0 { + if (1.0..=4.0).contains(&size) { settings.size = size; } } diff --git a/src-tauri/src/keyboard/daemon/mod.rs b/src-tauri/src/keyboard/daemon/mod.rs index ad1f583b..dfec6281 100644 --- a/src-tauri/src/keyboard/daemon/mod.rs +++ b/src-tauri/src/keyboard/daemon/mod.rs @@ -57,7 +57,7 @@ pub fn run() -> Result<()> { #[cfg(target_os = "macos")] { - return macos::run_macos(); + macos::run_macos() } #[cfg(not(any(target_os = "windows", target_os = "macos")))] diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 506ddb6d..2c6916af 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -220,6 +220,8 @@ fn main() { commands::keys::key_sound::key_sound_get_output_state, commands::keys::sound::sound_load, commands::keys::sound::sound_list, + commands::keys::sound::sound_set_hidden, + commands::keys::sound::sound_set_enabled, commands::keys::sound::sound_rename, commands::keys::sound::sound_delete, commands::keys::sound::sound_save_processed_wav, @@ -818,8 +820,8 @@ fn request_accessibility_permission() { keys.as_ptr(), values.as_ptr(), 1, - &kCFTypeDictionaryKeyCallBacks as *const _ as *const c_void, - &kCFTypeDictionaryValueCallBacks as *const _ as *const c_void, + &kCFTypeDictionaryKeyCallBacks as *const c_void, + &kCFTypeDictionaryValueCallBacks as *const c_void, ); let trusted = AXIsProcessTrustedWithOptions(options); diff --git a/src-tauri/src/models/mod.rs b/src-tauri/src/models/mod.rs index 89a4adf1..22581232 100644 --- a/src-tauri/src/models/mod.rs +++ b/src-tauri/src/models/mod.rs @@ -65,6 +65,8 @@ fn default_sound_source() -> SoundSource { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct SoundLibraryEntry { + #[serde(default)] + pub hidden: bool, #[serde(default = "default_sound_source")] pub source: SoundSource, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -80,6 +82,7 @@ pub struct SoundLibraryEntry { impl Default for SoundLibraryEntry { fn default() -> Self { Self { + hidden: false, source: SoundSource::Local, original_path: None, trim_start_ratio: None, @@ -573,7 +576,7 @@ pub fn normalize_user_counter_animation_presets( }) .collect(); - normalized.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase())); + normalized.sort_by_key(|a| a.name.to_lowercase()); normalized } diff --git a/src-tauri/src/state/app_state.rs b/src-tauri/src/state/app_state.rs index 090fa5f3..0f1b3eae 100644 --- a/src-tauri/src/state/app_state.rs +++ b/src-tauri/src/state/app_state.rs @@ -1087,7 +1087,7 @@ impl AppState { if emitted { keys_state_emit_count += 1; - if keys_state_emit_count % 500 == 0 { + if keys_state_emit_count.is_multiple_of(500) { log::debug!( "[AppState] emitted keys:state {} times (last key={}, state={})", keys_state_emit_count, @@ -1342,14 +1342,13 @@ impl AppState { #[cfg(target_os = "macos")] apply_macos_overlay_fullscreen_behavior(&overlay_window, snapshot.always_on_top); } - WindowEvent::Moved(_) | WindowEvent::Resized(_) => { + WindowEvent::Moved(_) | WindowEvent::Resized(_) // 윈도우 초기화 중에는 OS가 보고하는 좌표로 저장된 bounds를 덮어쓰지 않음 - if !initializing_flag.load(Ordering::SeqCst) { + if !initializing_flag.load(Ordering::SeqCst) => { if let Err(err) = persist_overlay_bounds(&overlay_window, &store) { log::warn!("failed to persist overlay bounds: {err}"); } } - } _ => {} }); } diff --git a/src-tauri/src/state/builtin_sounds.rs b/src-tauri/src/state/builtin_sounds.rs index 588e0175..1d8b4fc4 100644 --- a/src-tauri/src/state/builtin_sounds.rs +++ b/src-tauri/src/state/builtin_sounds.rs @@ -100,8 +100,13 @@ mod tests { assert_eq!(entry.display_name.as_deref(), Some(sound.display_name)); } + for entry in data.sound_library.values_mut() { + entry.hidden = true; + } + // 재실행: 변경 없음 (멱등) assert!(!seed_builtin_sounds(&dir, &mut data)); + assert!(data.sound_library.values().all(|entry| entry.hidden)); // Local로 오염된 source는 자가 치유 for entry in data.sound_library.values_mut() { @@ -112,6 +117,7 @@ mod tests { .sound_library .values() .all(|entry| entry.source == SoundSource::Builtin)); + assert!(data.sound_library.values().all(|entry| entry.hidden)); let _ = fs::remove_dir_all(&dir); } diff --git a/src-tauri/src/state/migration.rs b/src-tauri/src/state/migration.rs index 7cfc921f..a3161697 100644 --- a/src-tauri/src/state/migration.rs +++ b/src-tauri/src/state/migration.rs @@ -25,24 +25,32 @@ const LEGACY_OVERLAY_HEIGHT: f64 = 320.0; pub(crate) fn load_store_from_path(path: &Path) -> Result<(AppStoreData, bool)> { let content = fs::read_to_string(path) .with_context(|| format!("failed to read store file at {}", path.display()))?; - let (state, needs_persist) = match serde_json::from_str::(&content) { - Ok(mut data) => { - let mut needs_persist = data.font_settings.custom_fonts.iter().any(|font| { - font.font_type == FontType::Local - && font - .css_content - .as_ref() - .map(|c| !c.trim().is_empty()) - .unwrap_or(false) - }); - // rgba로 깨진 noteBorderColor가 있으면 정규화 후 디스크에도 영속 (이슈 #73) - if has_convertible_note_border_color(&data) { - needs_persist = true; - } - if migrate_legacy_knob_sensitivity(&mut data) { - needs_persist = true; + let (state, needs_persist) = match serde_json::from_str::(&content) { + Ok(mut value) => { + let sound_library_migrated = migrate_sound_library_enabled(&mut value); + match serde_json::from_value::(value) { + Ok(mut data) => { + let mut needs_persist = sound_library_migrated + || data.font_settings.custom_fonts.iter().any(|font| { + font.font_type == FontType::Local + && font + .css_content + .as_ref() + .map(|c| !c.trim().is_empty()) + .unwrap_or(false) + }); + // rgba로 깨진 noteBorderColor가 있으면 정규화 후 디스크에도 영속 (이슈 #73) + if has_convertible_note_border_color(&data) { + needs_persist = true; + } + if migrate_legacy_knob_sensitivity(&mut data) { + needs_persist = true; + } + (normalize_state(data), needs_persist) + } + // 레거시/비정상 store 파일 복구 후 정규화 상태 저장 + Err(_) => (repair_legacy_state(&content), true), } - (normalize_state(data), needs_persist) } // 레거시/비정상 store 파일 복구 후 정규화 상태 저장 Err(_) => (repair_legacy_state(&content), true), @@ -56,6 +64,27 @@ pub(crate) fn load_store_from_path(path: &Path) -> Result<(AppStoreData, bool)> Ok((state, needs_persist)) } +fn migrate_sound_library_enabled(value: &mut Value) -> bool { + let Some(sound_library) = value.get_mut("soundLibrary").and_then(Value::as_object_mut) else { + return false; + }; + + let mut changed = false; + for entry in sound_library.values_mut().filter_map(Value::as_object_mut) { + let Some(enabled) = entry.remove("enabled") else { + continue; + }; + + changed = true; + if !entry.contains_key("hidden") { + if let Some(enabled) = enabled.as_bool() { + entry.insert("hidden".to_string(), Value::Bool(!enabled)); + } + } + } + changed +} + /// 레거시 노브 민감도 마이그레이션: 도수/카운트(구 기본 1.40625) → 순수 배율(1.0) /// 배율 의미 전환 이전에 저장된 기본값만 1로 재매핑 (사용자 지정값은 유지) fn migrate_legacy_knob_sensitivity(data: &mut AppStoreData) -> bool { @@ -680,7 +709,86 @@ struct LegacyOverlayPosition { #[cfg(test)] mod tests { - use super::rgba_to_hex; + use super::{load_store_from_path, migrate_sound_library_enabled, rgba_to_hex}; + use crate::models::AppStoreData; + use serde_json::{json, Value}; + + const TEST_SOUND_PATH: &str = "/tmp/test-sound.wav"; + + fn load_store_with_sound_entry(entry: Value) -> (AppStoreData, bool) { + let path = std::env::temp_dir().join(format!( + "dmnote-sound-migration-test-{}.json", + uuid::Uuid::new_v4() + )); + let mut value = serde_json::to_value(AppStoreData::default()).unwrap(); + value.as_object_mut().unwrap().insert( + "soundLibrary".to_string(), + json!({ TEST_SOUND_PATH: entry }), + ); + std::fs::write(&path, serde_json::to_vec(&value).unwrap()).unwrap(); + + let result = load_store_from_path(&path).unwrap(); + let _ = std::fs::remove_file(path); + result + } + + #[test] + fn sound_library_enabled_false_migrates_to_hidden_true() { + let mut value = json!({ + "soundLibrary": { + TEST_SOUND_PATH: { "enabled": false } + } + }); + assert!(migrate_sound_library_enabled(&mut value)); + let entry = &value["soundLibrary"][TEST_SOUND_PATH]; + assert_eq!(entry["hidden"], true); + assert!(entry.get("enabled").is_none()); + + let (data, needs_persist) = load_store_with_sound_entry(json!({ "enabled": false })); + assert!(data.sound_library[TEST_SOUND_PATH].hidden); + assert!(needs_persist); + } + + #[test] + fn sound_library_enabled_true_migrates_to_hidden_false() { + let (data, needs_persist) = load_store_with_sound_entry(json!({ "enabled": true })); + assert!(!data.sound_library[TEST_SOUND_PATH].hidden); + assert!(needs_persist); + } + + #[test] + fn sound_library_without_enabled_is_unchanged() { + let mut value = json!({ + "soundLibrary": { + TEST_SOUND_PATH: { "source": "local" } + } + }); + let original = value.clone(); + assert!(!migrate_sound_library_enabled(&mut value)); + assert_eq!(value, original); + + let (data, needs_persist) = load_store_with_sound_entry(json!({ "source": "local" })); + assert!(!data.sound_library[TEST_SOUND_PATH].hidden); + assert!(!needs_persist); + } + + #[test] + fn sound_library_hidden_takes_precedence_over_enabled() { + let mut value = json!({ + "soundLibrary": { + TEST_SOUND_PATH: { "hidden": false, "enabled": false } + } + }); + assert!(migrate_sound_library_enabled(&mut value)); + let entry = &value["soundLibrary"][TEST_SOUND_PATH]; + assert_eq!(entry["hidden"], false); + assert!(entry.get("enabled").is_none()); + + let (data, needs_persist) = + load_store_with_sound_entry(json!({ "hidden": false, "enabled": false })); + assert!(!data.sound_library[TEST_SOUND_PATH].hidden); + assert!(needs_persist); + } #[test] fn rgba_to_hex_converts_and_drops_alpha() { diff --git a/src/renderer/api/modules/resourceApi.ts b/src/renderer/api/modules/resourceApi.ts index 5baa97d6..7514ae3b 100644 --- a/src/renderer/api/modules/resourceApi.ts +++ b/src/renderer/api/modules/resourceApi.ts @@ -25,6 +25,17 @@ export const soundApi = { invoke('sound_delete', { soundPath, }), + setHidden: (soundPath: string, hidden: boolean) => + invoke( + 'sound_set_hidden', + { soundPath, hidden }, + ), + // deprecated — setHidden의 역논리 별칭 (enabled = !hidden) + setEnabled: (soundPath: string, enabled: boolean) => + invoke( + 'sound_set_enabled', + { soundPath, enabled }, + ), saveProcessedWav: ( wavBase64: string, fileName?: string, diff --git a/src/renderer/components/main/Grid/PropertiesPanel.tsx b/src/renderer/components/main/Grid/PropertiesPanel.tsx index 771e8269..2c5892ab 100644 --- a/src/renderer/components/main/Grid/PropertiesPanel.tsx +++ b/src/renderer/components/main/Grid/PropertiesPanel.tsx @@ -2588,6 +2588,7 @@ const PropertiesPanel: React.FC = ({ onClick={handleTogglePanel} className="m-[8px] w-[32px] h-[32px] flex items-center justify-center bg-glass-dim backdrop-blur-[24px] rounded-[8px] shadow-elevation-chrome text-white/45 hover:text-white/90 transition-colors" title={t('propertiesPanel.openPanel') || '속성 패널 열기'} + aria-label={t('propertiesPanel.openPanel') || '속성 패널 열기'} > diff --git a/src/renderer/components/main/Grid/PropertiesPanel/PluginSettingsPanelView.tsx b/src/renderer/components/main/Grid/PropertiesPanel/PluginSettingsPanelView.tsx index 545bfb81..6500b55a 100644 --- a/src/renderer/components/main/Grid/PropertiesPanel/PluginSettingsPanelView.tsx +++ b/src/renderer/components/main/Grid/PropertiesPanel/PluginSettingsPanelView.tsx @@ -56,6 +56,7 @@ const PluginSettingsPanelView: React.FC = ({ onClick={handlePluginSettingsPanelCancel} className="w-[24px] h-[24px] -my-[6px] flex items-center justify-center text-white/45 hover:text-white/90 transition-colors" title={t('propertiesPanel.closePanel') || '속성 패널 닫기'} + aria-label={t('propertiesPanel.closePanel') || '속성 패널 닫기'} > diff --git a/src/renderer/components/main/Grid/PropertiesPanel/batch/BatchCounterTabContent.tsx b/src/renderer/components/main/Grid/PropertiesPanel/batch/BatchCounterTabContent.tsx index aa27ca9d..419a23e0 100644 --- a/src/renderer/components/main/Grid/PropertiesPanel/batch/BatchCounterTabContent.tsx +++ b/src/renderer/components/main/Grid/PropertiesPanel/batch/BatchCounterTabContent.tsx @@ -1,4 +1,4 @@ -import React, { useRef } from 'react'; +import React from 'react'; import { createPortal } from 'react-dom'; import type { KeyCounterSettings } from '@src/types/key/keys'; import { @@ -78,8 +78,6 @@ const BatchCounterTabContent: React.FC = ({ isStrokePickerOpen, t, }) => { - const fontButtonRef = useRef(null); - const animationButtonRef = useRef(null); // 인-패널 내비게이션 (폰트/애니메이션 서브 페이지) const { activePageKey, renderPageKey, openPage, closePage, pageHost } = usePanelNav(); @@ -248,7 +246,6 @@ const BatchCounterTabContent: React.FC = ({ {/* 폰트 */} @@ -592,6 +595,7 @@ export const BatchKeyLikePanel: React.FC = ({ onClick={handleTogglePanel} className="w-[24px] h-[24px] -my-[6px] flex items-center justify-center text-white/45 hover:text-white/90 transition-colors" title={t('propertiesPanel.closePanel') || '속성 패널 닫기'} + aria-label={t('propertiesPanel.closePanel') || '속성 패널 닫기'} > @@ -1138,6 +1142,9 @@ export const BatchGraphOnlyPanel: React.FC = ({ onClick={handleToggleMode} className="w-[24px] h-[24px] -my-[6px] flex items-center justify-center text-white/45 hover:text-white/90 transition-colors" title={t('propertiesPanel.switchToLayer') || 'Switch to Layer'} + aria-label={ + t('propertiesPanel.switchToLayer') || 'Switch to Layer' + } > @@ -1145,6 +1152,7 @@ export const BatchGraphOnlyPanel: React.FC = ({ onClick={handleTogglePanel} className="w-[24px] h-[24px] -my-[6px] flex items-center justify-center text-white/45 hover:text-white/90 transition-colors" title={t('propertiesPanel.closePanel') || '속성 패널 닫기'} + aria-label={t('propertiesPanel.closePanel') || '속성 패널 닫기'} > @@ -1494,6 +1502,9 @@ export const BatchKnobOnlyPanel: React.FC = ({ onClick={handleToggleMode} className="w-[24px] h-[24px] -my-[6px] flex items-center justify-center text-white/45 hover:text-white/90 transition-colors" title={t('propertiesPanel.switchToLayer') || 'Switch to Layer'} + aria-label={ + t('propertiesPanel.switchToLayer') || 'Switch to Layer' + } > @@ -1501,6 +1512,7 @@ export const BatchKnobOnlyPanel: React.FC = ({ onClick={handleTogglePanel} className="w-[24px] h-[24px] -my-[6px] flex items-center justify-center text-white/45 hover:text-white/90 transition-colors" title={t('propertiesPanel.closePanel') || '속성 패널 닫기'} + aria-label={t('propertiesPanel.closePanel') || '속성 패널 닫기'} > diff --git a/src/renderer/components/main/Grid/PropertiesPanel/batch/BatchStyleTabContent.tsx b/src/renderer/components/main/Grid/PropertiesPanel/batch/BatchStyleTabContent.tsx index ea530f5d..eb0d78ce 100644 --- a/src/renderer/components/main/Grid/PropertiesPanel/batch/BatchStyleTabContent.tsx +++ b/src/renderer/components/main/Grid/PropertiesPanel/batch/BatchStyleTabContent.tsx @@ -183,8 +183,6 @@ const BatchStyleTabContent: React.FC = ({ } }; }, []); - const fontButtonRef = useRef(null); - const soundButtonRef = useRef(null); // displayText의 실제 표시 값(displayText || keyInfo.displayName)을 기준으로 Mixed 판단 const getDisplayTextMixed = (): { isMixed: boolean; value: string } => { @@ -715,7 +713,6 @@ const BatchStyleTabContent: React.FC = ({ Mixed ) : null} @@ -148,6 +151,7 @@ const LayerPanel: React.FC = ({ onClick={onClose} className="w-[24px] h-[24px] -my-[6px] flex items-center justify-center text-white/45 hover:text-white/90 transition-colors" title={t('propertiesPanel.closePanel') || 'Close Panel'} + aria-label={t('propertiesPanel.closePanel') || 'Close Panel'} > diff --git a/src/renderer/components/main/Grid/PropertiesPanel/layer/LayerTabContent.tsx b/src/renderer/components/main/Grid/PropertiesPanel/layer/LayerTabContent.tsx index a37a8e15..0fbd9796 100644 --- a/src/renderer/components/main/Grid/PropertiesPanel/layer/LayerTabContent.tsx +++ b/src/renderer/components/main/Grid/PropertiesPanel/layer/LayerTabContent.tsx @@ -795,7 +795,6 @@ const LayerTabContent: React.FC = ({ }} items={actions.contextMenuItems} onSelect={actions.handleContextMenuSelect} - className="!z-[10000]" />, document.body, )} diff --git a/src/renderer/components/main/Grid/PropertiesPanel/single/CounterTabContent.tsx b/src/renderer/components/main/Grid/PropertiesPanel/single/CounterTabContent.tsx index 977f6e71..ffca2e97 100644 --- a/src/renderer/components/main/Grid/PropertiesPanel/single/CounterTabContent.tsx +++ b/src/renderer/components/main/Grid/PropertiesPanel/single/CounterTabContent.tsx @@ -38,8 +38,6 @@ const CounterTabContent: React.FC = ({ }) => { const fillBtnRef = useRef(null); const strokeBtnRef = useRef(null); - const fontBtnRef = useRef(null); - const animationBtnRef = useRef(null); const [pickerFor, setPickerFor] = useState(null); const pickerOpen = pickerFor !== null; @@ -307,7 +305,6 @@ const CounterTabContent: React.FC = ({ {/* 폰트 */} @@ -149,6 +150,7 @@ export const PluginSelectionPanel: React.FC = ({ onClick={handleTogglePanel} className="w-[24px] h-[24px] -my-[6px] flex items-center justify-center text-white/45 hover:text-white/90 transition-colors" title={t('propertiesPanel.closePanel') || '속성 패널 닫기'} + aria-label={t('propertiesPanel.closePanel') || '속성 패널 닫기'} > @@ -355,6 +357,7 @@ export const SingleGraphPanel: React.FC = ({ onClick={handleToggleMode} className="w-[24px] h-[24px] -my-[6px] flex items-center justify-center text-white/45 hover:text-white/90 transition-colors" title={t('propertiesPanel.switchToLayer') || 'Switch to Layer'} + aria-label={t('propertiesPanel.switchToLayer') || 'Switch to Layer'} > @@ -362,6 +365,7 @@ export const SingleGraphPanel: React.FC = ({ onClick={handleTogglePanel} className="w-[24px] h-[24px] -my-[6px] flex items-center justify-center text-white/45 hover:text-white/90 transition-colors" title={t('propertiesPanel.closePanel') || 'Close'} + aria-label={t('propertiesPanel.closePanel') || 'Close'} > @@ -994,6 +998,7 @@ export const SingleKnobPanel: React.FC = ({ onClick={handleToggleMode} className="w-[24px] h-[24px] -my-[6px] flex items-center justify-center text-white/45 hover:text-white/90 transition-colors" title={t('propertiesPanel.switchToLayer') || 'Switch to Layer'} + aria-label={t('propertiesPanel.switchToLayer') || 'Switch to Layer'} > @@ -1001,6 +1006,7 @@ export const SingleKnobPanel: React.FC = ({ onClick={handleTogglePanel} className="w-[24px] h-[24px] -my-[6px] flex items-center justify-center text-white/45 hover:text-white/90 transition-colors" title={t('propertiesPanel.closePanel') || 'Close'} + aria-label={t('propertiesPanel.closePanel') || 'Close'} > @@ -1554,6 +1560,9 @@ export const SingleKeyStatPanel: React.FC = ({ onClick={handleToggleMode} className="w-[24px] h-[24px] -my-[6px] flex items-center justify-center text-white/45 hover:text-white/90 transition-colors" title={t('propertiesPanel.switchToLayer') || 'Switch to Layer'} + aria-label={ + t('propertiesPanel.switchToLayer') || 'Switch to Layer' + } > @@ -1562,6 +1571,7 @@ export const SingleKeyStatPanel: React.FC = ({ onClick={handleTogglePanel} className="w-[24px] h-[24px] -my-[6px] flex items-center justify-center text-white/45 hover:text-white/90 transition-colors" title={t('propertiesPanel.closePanel') || '속성 패널 닫기'} + aria-label={t('propertiesPanel.closePanel') || '속성 패널 닫기'} > diff --git a/src/renderer/components/main/Grid/PropertiesPanel/single/StyleTabContent.tsx b/src/renderer/components/main/Grid/PropertiesPanel/single/StyleTabContent.tsx index 6ccd11c3..f69da6ef 100644 --- a/src/renderer/components/main/Grid/PropertiesPanel/single/StyleTabContent.tsx +++ b/src/renderer/components/main/Grid/PropertiesPanel/single/StyleTabContent.tsx @@ -104,8 +104,6 @@ const StyleTabContent: React.FC = ({ // 컬러 버튼 refs const bgColorBtnRef = useRef(null); // 폰트 버튼 ref - const fontButtonRef = useRef(null); - const soundButtonRef = useRef(null); const borderColorBtnRef = useRef(null); const fontColorBtnRef = useRef(null); const internalImageButtonRef = useRef(null); @@ -591,7 +589,6 @@ const StyleTabContent: React.FC = ({ {/* 폰트 */}
diff --git a/src/renderer/components/main/Modal/ManagerModalLayout.tsx b/src/renderer/components/main/Modal/ManagerModalLayout.tsx index bd670d59..68631b36 100644 --- a/src/renderer/components/main/Modal/ManagerModalLayout.tsx +++ b/src/renderer/components/main/Modal/ManagerModalLayout.tsx @@ -5,7 +5,6 @@ import React, { type ReactNode, } from 'react'; import { useLenis } from '@hooks/useLenis'; -import { getScrollShadowState } from '@utils/grid/scrollShadow'; import Modal from './Modal'; const MAX_SCROLL_HEIGHT = 195; @@ -35,29 +34,11 @@ const ManagerModalLayout = ({ contentDeps = [], }: ManagerModalLayoutProps) => { const contentRef = useRef(null); - const [scrollState, setScrollState] = useState({ - hasTopShadow: false, - hasBottomShadow: false, - }); - const [skipShadowTransition, setSkipShadowTransition] = useState(true); const [containerHeight, setContainerHeight] = useState(null); const [isScrollable, setIsScrollable] = useState(false); const isFirstRender = useRef(true); - const updateScrollState = (el: HTMLElement | null) => { - if (!el) return; - const nextState = getScrollShadowState(el, contentRef.current); - setScrollState((prev) => - prev.hasTopShadow === nextState.hasTopShadow && - prev.hasBottomShadow === nextState.hasBottomShadow - ? prev - : nextState, - ); - }; - - const { scrollContainerRef: scrollRef, wrapperElement } = useLenis({ - onScroll: () => updateScrollState(wrapperElement), - }); + const { scrollContainerRef: scrollRef } = useLenis(); useLayoutEffect(() => { if (!isOpen) { @@ -65,35 +46,23 @@ const ManagerModalLayout = ({ return; } - setSkipShadowTransition(true); - setScrollState({ hasTopShadow: false, hasBottomShadow: false }); setIsScrollable(false); - const el = wrapperElement; const contentEl = contentRef.current; - if (!el) return; + if (!contentEl) return; const updateHeight = () => { - if (contentEl) { - const contentHeight = contentEl.scrollHeight; - setContainerHeight(Math.min(contentHeight, MAX_SCROLL_HEIGHT)); - setIsScrollable(contentHeight > MAX_SCROLL_HEIGHT); - } + const contentHeight = contentEl.scrollHeight; + setContainerHeight(Math.min(contentHeight, MAX_SCROLL_HEIGHT)); + setIsScrollable(contentHeight > MAX_SCROLL_HEIGHT); }; - const resizeObserver = new ResizeObserver(() => { - updateScrollState(el); - updateHeight(); - }); - - if (contentEl) resizeObserver.observe(contentEl); - resizeObserver.observe(el); - - updateScrollState(el); + // 콘텐츠 크기 변경 감지 (높이 애니메이션용) + const resizeObserver = new ResizeObserver(updateHeight); + resizeObserver.observe(contentEl); updateHeight(); const rafId = requestAnimationFrame(() => { - setSkipShadowTransition(false); isFirstRender.current = false; }); @@ -102,14 +71,10 @@ const ManagerModalLayout = ({ cancelAnimationFrame(rafId); }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isOpen, wrapperElement, ...contentDeps]); + }, [isOpen, ...contentDeps]); if (!isOpen) return null; - const shadowTransitionClass = skipShadowTransition - ? '' - : 'transition-opacity duration-150'; - return (
{tabs}
} {/* 스크롤 영역 */} -
- {/* 상단 그림자 */} -
- -
-
- {children} -
+
+
+ {children}
- - {/* 하단 그림자 */} -
{/* 구분선 */} diff --git a/src/renderer/components/main/Modal/content/dialogs/Alert.tsx b/src/renderer/components/main/Modal/content/dialogs/Alert.tsx index 3456e473..6eac425a 100644 --- a/src/renderer/components/main/Modal/content/dialogs/Alert.tsx +++ b/src/renderer/components/main/Modal/content/dialogs/Alert.tsx @@ -1,8 +1,7 @@ -import React, { useState, useEffect } from 'react'; +import React from 'react'; import { useLenis } from '@hooks/useLenis'; import { useTranslation } from '@contexts/useTranslation'; import Modal from '../../Modal'; -import { getScrollShadowState } from '@utils/grid/scrollShadow'; interface AlertProps { isOpen: boolean; @@ -27,42 +26,11 @@ const Alert = ({ }: AlertProps) => { const { t } = useTranslation(); - const [scrollState, setScrollState] = useState<{ - hasTopShadow: boolean; - hasBottomShadow: boolean; - }>({ - hasTopShadow: false, - hasBottomShadow: false, - }); - - // 스크롤 상태 업데이트 함수 - const updateScrollState = (el: HTMLElement | null) => { - if (!el) return; - const nextState = getScrollShadowState(el); - setScrollState((prev) => - prev.hasTopShadow === nextState.hasTopShadow && - prev.hasBottomShadow === nextState.hasBottomShadow - ? prev - : nextState, - ); - }; - - // Lenis smooth scroll 적용 (onScroll 콜백으로 그림자 업데이트) - const { scrollContainerRef: scrollRef, wrapperElement } = useLenis({ - onScroll: () => updateScrollState(wrapperElement), - }); + const { scrollContainerRef: scrollRef } = useLenis(); const isConfirm = type === 'confirm'; const isCustom = type === 'custom'; - useEffect(() => { - if (isCustom && wrapperElement) { - // DOM이 렌더링된 후 스크롤 상태 확인 - const timer = setTimeout(() => updateScrollState(wrapperElement), 0); - return () => clearTimeout(timer); - } - }, [isCustom, message, wrapperElement]); - if (!isOpen) return null; const confirmLabel = confirmText || t('common.confirm'); @@ -77,27 +45,11 @@ const Alert = ({ > {/* 메시지 텍스트 or Custom HTML */} {isCustom ? ( -
- {/* 상단 그림자 */} -
- -
- - {/* 하단 그림자 */} -
-
+
) : (
{message} diff --git a/src/renderer/components/main/Modal/content/dialogs/UnifiedKeySetting.tsx b/src/renderer/components/main/Modal/content/dialogs/UnifiedKeySetting.tsx index a407b68a..85e27386 100644 --- a/src/renderer/components/main/Modal/content/dialogs/UnifiedKeySetting.tsx +++ b/src/renderer/components/main/Modal/content/dialogs/UnifiedKeySetting.tsx @@ -25,7 +25,6 @@ import { type SaveData, type PreviewData, } from '@hooks/Modal/useUnifiedKeySettingState'; -import { getScrollShadowState } from '@utils/grid/scrollShadow'; import type { KeyCounterSettings } from '@src/types/key/keys'; // ============================================================================ @@ -56,13 +55,6 @@ const UnifiedKeySetting: React.FC = ({ const { t } = useTranslation(); const initialSkipRef = React.useRef(skipAnimation); const contentRef = React.useRef(null); - const [scrollState, setScrollState] = React.useState({ - hasTopShadow: false, - hasBottomShadow: false, - }); - const [hasOverflow, setHasOverflow] = React.useState(false); - // 탭 전환 시 그림자 애니메이션 스킵 여부 (깜빡임 방지) - const [skipShadowTransition, setSkipShadowTransition] = React.useState(false); // 컨테이너 높이 (애니메이션용) const [containerHeight, setContainerHeight] = React.useState( null, @@ -75,26 +67,7 @@ const UnifiedKeySetting: React.FC = ({ const noteTabRef = React.useRef(null); const counterTabRef = React.useRef(null); - // 스크롤 상태 업데이트 함수 - const updateScrollState = (el: HTMLElement | null) => { - if (!el) return; - const nextState = getScrollShadowState(el, contentRef.current); - setScrollState((prev) => - prev.hasTopShadow === nextState.hasTopShadow && - prev.hasBottomShadow === nextState.hasBottomShadow - ? prev - : nextState, - ); - }; - - // Lenis smooth scroll 적용 (onScroll 콜백으로 그림자 업데이트) - const { - scrollContainerRef: scrollRef, - wrapperElement, - lenisInstance, - } = useLenis({ - onScroll: () => updateScrollState(wrapperElement), - }); + const { scrollContainerRef: scrollRef, lenisInstance } = useLenis(); const { activeTab, @@ -118,46 +91,24 @@ const UnifiedKeySetting: React.FC = ({ onClose, }); - // 탭 변경 또는 마운트 시 스크롤 상태 확인 (DOM 렌더링 후 확인) + // 탭 변경 또는 마운트 시 콘텐츠 높이 동기화 (높이 애니메이션용) React.useEffect(() => { - // 탭 전환 시 그림자 애니메이션 스킵 - setSkipShadowTransition(true); - - // 콘텐츠 크기 변경 감지를 위한 ResizeObserver - const el = wrapperElement; const contentEl = contentRef.current; - if (!el) return; + if (!contentEl) return; const updateHeight = () => { - if (contentEl) { - const contentHeight = contentEl.scrollHeight; - const maxHeight = 195; - setContainerHeight(Math.min(contentHeight, maxHeight)); - const nextHasOverflow = contentHeight > maxHeight; - setHasOverflow((prev) => - prev === nextHasOverflow ? prev : nextHasOverflow, - ); - } + const contentHeight = contentEl.scrollHeight; + const maxHeight = 195; + setContainerHeight(Math.min(contentHeight, maxHeight)); }; - const resizeObserver = new ResizeObserver(() => { - updateScrollState(el); - updateHeight(); - }); - // 스크롤 영역 내부의 콘텐츠 크기 변경을 감지 - if (contentEl) { - resizeObserver.observe(contentEl); - } - resizeObserver.observe(el); - - // 초기 상태 확인 - updateScrollState(el); + const resizeObserver = new ResizeObserver(updateHeight); + resizeObserver.observe(contentEl); updateHeight(); - // 다음 프레임에서 애니메이션 다시 활성화 및 첫 렌더 플래그 해제 + // 다음 프레임에서 첫 렌더 플래그 해제 const rafId = requestAnimationFrame(() => { - setSkipShadowTransition(false); isFirstRender.current = false; }); @@ -165,7 +116,7 @@ const UnifiedKeySetting: React.FC = ({ resizeObserver.disconnect(); cancelAnimationFrame(rafId); }; - }, [activeTab, wrapperElement]); + }, [activeTab]); // 탭 변경 시 스크롤 최상단으로 초기화 React.useEffect(() => { @@ -250,39 +201,18 @@ const UnifiedKeySetting: React.FC = ({
{/* 스크롤 영역 - 스크롤바가 모달 오른쪽 끝에 위치 */} -
- {/* 상단 그림자 */} -
- -
-
{renderTabContent()}
-
- - {/* 하단 그림자 */} -
+
+
{renderTabContent()}
{/* 저장/취소 버튼 */} diff --git a/src/renderer/components/main/Modal/content/pickers/CommonListPickerPage.tsx b/src/renderer/components/main/Modal/content/pickers/CommonListPickerPage.tsx new file mode 100644 index 00000000..8dd45df8 --- /dev/null +++ b/src/renderer/components/main/Modal/content/pickers/CommonListPickerPage.tsx @@ -0,0 +1,188 @@ +import React, { useEffect } from 'react'; +import { useLenis } from '@hooks/useLenis'; +import Dropdown from '@components/main/common/Dropdown'; + +type FilterOption = { + value: string; + label: string; +}; + +interface CommonListPickerPageProps { + open: boolean; + searchQuery: string; + onSearchQueryChange: (value: string) => void; + searchPlaceholder: string; + filterOptions?: FilterOption[]; + filterValue?: string; + onFilterChange?: (value: string) => void; + items: T[]; + renderItem: (item: T) => React.ReactNode; + emptyText: string; + isLoading?: boolean; + loadingText?: string; + errorText?: string; + onAdd: (event: React.MouseEvent) => void; + addLabel: string; + // 추가 버튼에 앵커된 메뉴의 기준점용 + addButtonRef?: React.RefObject; + pageTitle: string; + onBack: () => void; +} + +export default function CommonListPickerPage({ + open, + searchQuery, + onSearchQueryChange, + searchPlaceholder, + filterOptions, + filterValue, + onFilterChange, + items, + renderItem, + emptyText, + isLoading = false, + loadingText = '로딩...', + errorText = '', + onAdd, + addLabel, + addButtonRef, + pageTitle, + onBack, +}: CommonListPickerPageProps) { + const { scrollContainerRef: scrollRef, lenisInstance } = useLenis(); + + useEffect(() => { + if (!open) return; + const rafId = requestAnimationFrame(() => { + lenisInstance.current?.resize?.(); + }); + return () => cancelAnimationFrame(rafId); + }, [open, items.length, filterValue, searchQuery, lenisInstance]); + + if (!open) return null; + + return ( +
+ {/* 헤더 — 뒤로가기: 타이틀 캡하이트급 셰브론 + 타이틀 통짜 버튼 */} +
+ +
+ {/* 검색 — 리스트와 인접한 프라이머리 컨트롤 */} +
+
+ + + + + onSearchQueryChange(event.target.value)} + placeholder={searchPlaceholder} + className="w-full h-[30px] pl-[30px] pr-[10px] bg-inset rounded-surface text-fg text-body placeholder-fg-faint focus:shadow-focus-ring outline-none transition-shadow duration-fast" + /> +
+
+ {/* 리스트 웰 — 리세스드 테이블. 빈 공간도 테이블의 빈 영역으로 읽힘 */} +
+
+ {/* flex-1로 스크롤 영역을 채워 빈 상태가 중앙에 오게 함 */} +
+ {items.length === 0 && !isLoading && !errorText ? ( +
+ {emptyText} +
+ ) : null} + {items.map((item) => renderItem(item))} + {isLoading ? ( +

+ {loadingText} +

+ ) : null} + {errorText ? ( +

+ {errorText} +

+ ) : null} +
+
+
+ {/* 하단 도구 바 — 필터 + 추가 (Xcode 내비게이터 문법) */} +
+
+ {filterOptions && filterValue !== undefined && onFilterChange ? ( +
+ +
+ ) : null} + +
+
+
+ ); +} diff --git a/src/renderer/components/main/Modal/content/pickers/CommonListPickerPopup.tsx b/src/renderer/components/main/Modal/content/pickers/CommonListPickerPopup.tsx deleted file mode 100644 index 98473adf..00000000 --- a/src/renderer/components/main/Modal/content/pickers/CommonListPickerPopup.tsx +++ /dev/null @@ -1,343 +0,0 @@ -/* eslint-disable react-hooks/set-state-in-effect */ -import React, { - useCallback, - useEffect, - useLayoutEffect, - useRef, - useState, -} from 'react'; -import FloatingPopup from '../../FloatingPopup'; -import { useLenis } from '@hooks/useLenis'; -import Dropdown from '@components/main/common/Dropdown'; - -// 팝업 셸 예상 폭 (측정 전 위치 계산용) -const ESTIMATED_WIDTH = 164; - -type FilterOption = { - value: string; - label: string; -}; - -interface CommonListPickerPopupProps { - open: boolean; - referenceRef: React.RefObject; - panelElement?: HTMLElement | null; - onClose: () => void; - interactiveRefs?: Array>; - estimatedHeight?: number; - searchQuery: string; - onSearchQueryChange: (value: string) => void; - searchPlaceholder: string; - filterOptions?: FilterOption[]; - filterValue?: string; - onFilterChange?: (value: string) => void; - items: T[]; - renderItem: (item: T) => React.ReactNode; - emptyText: string; - isLoading?: boolean; - loadingText?: string; - errorText?: string; - onAdd: (event: React.MouseEvent) => void; - addLabel: string; - // 추가 버튼에 앵커된 메뉴가 열릴 때 외부 클릭 판정 제외용 - addButtonRef?: React.RefObject; - // 'page': 사이드 패널 서브 페이지로 렌더 (플로팅 크롬 대신 뒤로가기 헤더) - renderMode?: 'popup' | 'page'; - pageTitle?: string; - onBack?: () => void; -} - -export default function CommonListPickerPopup({ - open, - referenceRef, - panelElement = null, - onClose, - interactiveRefs = [], - estimatedHeight = 280, - searchQuery, - onSearchQueryChange, - searchPlaceholder, - filterOptions, - filterValue, - onFilterChange, - items, - renderItem, - emptyText, - isLoading = false, - loadingText = '로딩...', - errorText = '', - onAdd, - addLabel, - addButtonRef, - renderMode = 'popup', - pageTitle, - onBack, -}: CommonListPickerPopupProps) { - const containerRef = useRef(null); - const [fixedPosition, setFixedPosition] = useState<{ - x: number; - y: number; - } | null>(null); - - const { scrollContainerRef: scrollRef, lenisInstance } = useLenis(); - - const calculatePosition = useCallback(() => { - if (!panelElement) return; - - const panelRect = panelElement.getBoundingClientRect(); - const popupEl = containerRef.current; - const popupWidth = popupEl ? popupEl.offsetWidth : ESTIMATED_WIDTH; - const popupHeight = popupEl ? popupEl.offsetHeight : estimatedHeight; - - const gap = 5; - const padding = 5; - const panelBottomPadding = 20; - - let fixedX = panelRect.left - popupWidth - gap; - if (fixedX < padding) { - fixedX = padding; - } - - let fixedY = panelRect.bottom - panelBottomPadding - popupHeight; - if (fixedY < padding) { - fixedY = padding; - } - - setFixedPosition((prev) => { - if (prev && prev.x === fixedX && prev.y === fixedY) return prev; - return { x: fixedX, y: fixedY }; - }); - }, [estimatedHeight, panelElement]); - - useLayoutEffect(() => { - if (!open) { - setFixedPosition(null); - return; - } - - if (!panelElement) { - setFixedPosition(null); - return; - } - - calculatePosition(); - }, [calculatePosition, open, panelElement]); - - // 팝업의 실제 크기가 바뀌면 위치 재계산 - useLayoutEffect(() => { - if (!open || !panelElement || !containerRef.current) return; - - const resizeObserver = new ResizeObserver(() => { - calculatePosition(); - }); - resizeObserver.observe(containerRef.current); - - return () => { - resizeObserver.disconnect(); - }; - }, [calculatePosition, open, panelElement]); - - useEffect(() => { - if (!open) return; - const rafId = requestAnimationFrame(() => { - lenisInstance.current?.resize?.(); - }); - return () => cancelAnimationFrame(rafId); - }, [open, items.length, filterValue, searchQuery, lenisInstance]); - - const effectiveOffsetY = fixedPosition ? 0 : -93; - - // 페이지 모드는 프라이머리 검색(h-30 + 돋보기 글리프), 팝업은 콤팩트 웰 - const searchInput = - renderMode === 'page' ? ( -
- - - - - onSearchQueryChange(event.target.value)} - placeholder={searchPlaceholder} - className="w-full h-[30px] pl-[30px] pr-[10px] bg-inset rounded-surface text-fg text-body placeholder-fg-faint focus:shadow-focus-ring outline-none transition-shadow duration-fast" - /> -
- ) : ( - onSearchQueryChange(event.target.value)} - placeholder={searchPlaceholder} - className="w-full h-[26px] px-[8px] bg-inset rounded-md text-fg text-body placeholder-fg-faint focus:shadow-focus-ring outline-none transition-shadow duration-fast" - /> - ); - - // 필터 + 추가 — 같은 재질의 칩 한 쌍. 페이지는 검색과 같은 30 크롬 스케일 - const controlChipClass = - renderMode === 'page' - ? 'w-[30px] h-[30px] rounded-surface' - : 'w-[24px] h-[24px] rounded-md'; - const filterAddRow = ( -
- {filterOptions && filterValue !== undefined && onFilterChange ? ( -
- -
- ) : null} - -
- ); - - // flex-1로 스크롤 영역을 채워 빈 상태가 중앙에 오게 함 - const listInner = ( -
- {items.length === 0 && !isLoading && !errorText ? ( -
- {emptyText} -
- ) : null} - {items.map((item) => renderItem(item))} - {isLoading ? ( -

- {loadingText} -

- ) : null} - {errorText ? ( -

- {errorText} -

- ) : null} -
- ); - - // 페이지 모드 — 패널 서브 페이지: 뒤로가기 헤더 + 전고 리스트 - if (renderMode === 'page') { - if (!open) return null; - return ( -
- {/* 헤더 — 뒤로가기: 타이틀 캡하이트급 셰브론 + 타이틀 통짜 버튼 */} -
- -
- {/* 검색 — 리스트와 인접한 프라이머리 컨트롤 */} -
{searchInput}
- {/* 리스트 웰 — 리세스드 테이블. 빈 공간도 테이블의 빈 영역으로 읽힘 */} -
-
- {listInner} -
-
- {/* 하단 도구 바 — 필터 + 추가 (Xcode 내비게이터 문법) */} -
{filterAddRow}
-
- ); - } - - return ( - -
- {searchInput} - {filterAddRow} - {/* 리스트 웰 — 면으로 영역 구분 */} -
-
- {listInner} -
-
-
-
- ); -} diff --git a/src/renderer/components/main/Modal/content/pickers/CounterAnimationPicker.tsx b/src/renderer/components/main/Modal/content/pickers/CounterAnimationPicker.tsx index 96b7d7e8..d058d9b4 100644 --- a/src/renderer/components/main/Modal/content/pickers/CounterAnimationPicker.tsx +++ b/src/renderer/components/main/Modal/content/pickers/CounterAnimationPicker.tsx @@ -14,14 +14,12 @@ import { } from '@src/types/key/counterAnimation'; import ListPopup, { type ListItem } from '@components/main/Modal/ListPopup'; import { usePickerItemMenu } from '@hooks/usePickerItemMenu'; -import CommonListPickerPopup from './CommonListPickerPopup'; +import CommonListPickerPage from './CommonListPickerPage'; import { pickerRowClass, pickerMoreButtonClass } from './pickerRowClass'; import CounterAnimationEditorModal from '../editors/CounterAnimationEditorModal'; interface CounterAnimationPickerProps { open: boolean; - referenceRef: React.RefObject; - panelElement?: HTMLElement | null; animation: KeyCounterAnimationSettings; counterSettings?: KeyCounterSettings; keyVisual?: { @@ -48,13 +46,9 @@ interface CounterAnimationPickerProps { isStat?: boolean; }; onAnimationChange: (next: KeyCounterAnimationSettings) => void; - onClose: () => void; t: (key: string) => string; - interactiveRefs?: Array>; - // 페이지 모드 패스스루 — 패널 서브 페이지로 렌더할 때 사용 - renderMode?: 'popup' | 'page'; - pageTitle?: string; - onBack?: () => void; + pageTitle: string; + onBack: () => void; } type FilterType = 'all' | 'builtin' | 'user'; @@ -83,16 +77,11 @@ const EMPTY_LIBRARY: CounterAnimationListResponse = { const CounterAnimationPicker = ({ open, - referenceRef, - panelElement = null, animation, counterSettings, keyVisual, onAnimationChange, - onClose, t, - interactiveRefs = [], - renderMode = 'popup', pageTitle, onBack, }: CounterAnimationPickerProps) => { @@ -240,23 +229,12 @@ const CounterAnimationPicker = ({ } }; - const handlePickerClose = () => { - if (menu.menuKey !== null) return; - onClose(); - }; - return ( <> - + open={open} - referenceRef={referenceRef} - panelElement={panelElement} - interactiveRefs={interactiveRefs} - renderMode={renderMode} pageTitle={pageTitle} onBack={onBack} - onClose={handlePickerClose} - estimatedHeight={276} searchQuery={searchQuery} onSearchQueryChange={setSearchQuery} searchPlaceholder={ @@ -286,7 +264,7 @@ const CounterAnimationPicker = ({ handlePresetSelect(preset); } }} - className={`${pickerRowClass(renderMode)} cursor-pointer ${ + className={`${pickerRowClass} cursor-pointer ${ isSelected ? 'bg-surface-active text-fg' : 'text-fg hover:bg-surface-hover' @@ -300,14 +278,14 @@ const CounterAnimationPicker = ({ {isUserPreset ? ( - ) : null} +
); }} @@ -332,6 +337,8 @@ const SoundPicker = ({ if (!item) return; if (id === 'edit') { setTrimState({ mode: 'edit', item }); + } else if (id === 'toggle-hidden') { + void handleToggleHidden(item); } else if (id === 'rename') { renameCancelledRef.current = false; setRenameValue(item.displayName || item.fileName); @@ -340,7 +347,6 @@ const SoundPicker = ({ void handleDelete(item); } }} - className="z-[60]" offsetX={0} offsetY={0} /> diff --git a/src/renderer/components/main/Modal/content/pickers/pickerRowClass.ts b/src/renderer/components/main/Modal/content/pickers/pickerRowClass.ts index 4c0e1560..5df9678d 100644 --- a/src/renderer/components/main/Modal/content/pickers/pickerRowClass.ts +++ b/src/renderer/components/main/Modal/content/pickers/pickerRowClass.ts @@ -1,14 +1,10 @@ -// 피커 리스트 행 규격 — 팝업(콤팩트 필)과 패널 페이지(인셋 웰 테이블 행) -export type PickerRenderMode = 'popup' | 'page'; +// 피커 리스트 행 규격 — 패널 페이지의 인셋 웰 테이블 행 -// 페이지 행: 인셋 웰(테이블) 내부의 필 행 — 30 컨트롤 스케일, 웰과 동심 라운딩 -export const pickerRowClass = (mode: PickerRenderMode): string => - mode === 'page' - ? 'w-full h-[30px] px-[8px] rounded-md text-style-4 transition-colors flex items-center gap-[4px] group' - : 'w-full h-[24px] px-[8px] rounded-md text-style-4 transition-colors flex items-center gap-[4px] group'; +// 인셋 웰(테이블) 내부의 필 행 — 30 컨트롤 스케일, 웰과 동심 라운딩 +export const pickerRowClass = + 'w-full h-[30px] px-[8px] rounded-md text-style-4 transition-colors flex items-center gap-[4px] group'; -// 행 트레일링 ⋮ 버튼 — 페이지는 28px(30 행 스케일), 팝업은 18px 옵티컬 정렬 -export const pickerMoreButtonClass = (mode: PickerRenderMode): string => - mode === 'page' - ? 'w-[28px] h-[28px] rounded-md transition-all flex items-center justify-center shrink-0' - : 'w-[18px] h-[18px] -mr-[8px] rounded-md transition-all flex items-center justify-center shrink-0'; +// 행 트레일링 ⋮ 버튼 — 28px(30 행 스케일). +// -mr-8: 버튼을 행 패딩 안쪽까지 확장해 아이콘 시각 인셋을 텍스트(8px)와 대칭으로 +export const pickerMoreButtonClass = + 'w-[28px] h-[28px] -mr-[8px] rounded-md transition-all flex items-center justify-center shrink-0'; diff --git a/src/renderer/components/main/common/Dropdown.tsx b/src/renderer/components/main/common/Dropdown.tsx index 88d41270..e08c6f75 100644 --- a/src/renderer/components/main/common/Dropdown.tsx +++ b/src/renderer/components/main/common/Dropdown.tsx @@ -84,6 +84,18 @@ const Dropdown: React.FC = ({ setOpen((prev) => !prev); }; + // Escape 소유 — 최상위 포털 레이어이므로 소비 후 자신만 닫기 + useEffect(() => { + if (!open) return; + const handleKey = (event: KeyboardEvent) => { + if (event.key !== 'Escape' || event.defaultPrevented) return; + event.preventDefault(); + setOpen(false); + }; + document.addEventListener('keydown', handleKey); + return () => document.removeEventListener('keydown', handleKey); + }, [open]); + useEffect(() => { if (!open) return; diff --git a/src/renderer/components/shared/PluginElement.tsx b/src/renderer/components/shared/PluginElement.tsx index 64807bbf..9d4f74ca 100644 --- a/src/renderer/components/shared/PluginElement.tsx +++ b/src/renderer/components/shared/PluginElement.tsx @@ -1612,7 +1612,6 @@ export const PluginElement: React.FC = ({ onClose={() => setContextMenuOpen(false)} items={contextMenuItems} onSelect={handleContextMenuSelect} - className="!z-[10000]" />, document.body, )} diff --git a/src/renderer/hooks/useLenis.ts b/src/renderer/hooks/useLenis.ts index ea304423..11f9c6b1 100644 --- a/src/renderer/hooks/useLenis.ts +++ b/src/renderer/hooks/useLenis.ts @@ -28,11 +28,6 @@ interface UseLenisOptions { * @default LENIS_CONFIG.touchMultiplier */ touchMultiplier?: number; - /** - * 스크롤 이벤트 콜백 - * Lenis 스크롤 발생 시 호출됨 - */ - onScroll?: () => void; } // easeOutExpo 이징 함수 @@ -49,12 +44,6 @@ export const useLenis = (options: UseLenisOptions = {}) => { const [wrapper, setWrapper] = useState(null); const wrapperRef = useRef(null); const lenisRef = useRef(null); - const onScrollRef = useRef<(() => void) | undefined>(options.onScroll); - - // onScroll 콜백 업데이트 - useEffect(() => { - onScrollRef.current = options.onScroll; - }, [options.onScroll]); const { duration, @@ -97,12 +86,6 @@ export const useLenis = (options: UseLenisOptions = {}) => { lenisRef.current = lenis; - // Lenis scroll 이벤트 리스너 등록 - const handleLenisScroll = () => { - onScrollRef.current?.(); - }; - lenis.on('scroll', handleLenisScroll); - // RAF 루프 시작 let rafId: number; const raf = (time: number) => { @@ -114,7 +97,6 @@ export const useLenis = (options: UseLenisOptions = {}) => { // 클린업 return () => { cancelAnimationFrame(rafId); - lenis.off('scroll', handleLenisScroll); lenis.destroy(); lenisRef.current = null; }; diff --git a/src/renderer/locales/en.json b/src/renderer/locales/en.json index 365b38bd..32be6984 100644 --- a/src/renderer/locales/en.json +++ b/src/renderer/locales/en.json @@ -568,13 +568,17 @@ "searchPlaceholder": "Search...", "filterAll": "All", "filterLocal": "Local Sounds", + "filterHidden": "Hidden Sounds", "noSounds": "No sounds", "loadFailed": "Failed to load sound list", "edit": "Edit", "rename": "Rename", + "hide": "Hide", + "unhide": "Unhide", "delete": "Delete", "deleteFailed": "Failed to delete sound", "renameFailed": "Failed to rename sound", + "hideFailed": "Failed to change sound visibility", "deleteConfirm": "Delete this sound? The file will be deleted and unassigned from every element using it.", "add": "Add Sound" }, diff --git a/src/renderer/locales/ko.json b/src/renderer/locales/ko.json index 080d68d6..f57524ce 100644 --- a/src/renderer/locales/ko.json +++ b/src/renderer/locales/ko.json @@ -568,13 +568,17 @@ "searchPlaceholder": "검색", "filterAll": "전체", "filterLocal": "로컬 사운드", + "filterHidden": "숨긴 사운드", "noSounds": "사운드 없음", "loadFailed": "사운드 목록 로드 실패", "edit": "편집", "rename": "이름 변경", + "hide": "숨기기", + "unhide": "숨김 해제", "delete": "삭제", "deleteFailed": "사운드 삭제 실패", "renameFailed": "사운드 이름 변경 실패", + "hideFailed": "사운드 숨김 변경 실패", "deleteConfirm": "사운드를 삭제하시겠습니까? 파일이 삭제되고 이 사운드를 사용하는 모든 요소에서 해제됩니다.", "add": "사운드 추가" }, diff --git a/src/renderer/locales/ru.json b/src/renderer/locales/ru.json index 7c77aa69..ddb8f642 100644 --- a/src/renderer/locales/ru.json +++ b/src/renderer/locales/ru.json @@ -568,13 +568,17 @@ "searchPlaceholder": "Поиск...", "filterAll": "Все", "filterLocal": "Локальные звуки", + "filterHidden": "Скрытые звуки", "noSounds": "Звуков нет", "loadFailed": "Не удалось загрузить список звуков", "edit": "Редактировать", "rename": "Переименовать", + "hide": "Скрыть", + "unhide": "Показать", "delete": "Удалить", "deleteFailed": "Не удалось удалить звук", "renameFailed": "Не удалось переименовать звук", + "hideFailed": "Не удалось изменить видимость звука", "deleteConfirm": "Удалить звук? Файл будет удалён, и звук будет снят со всех элементов, где он используется.", "add": "Добавить звук" }, diff --git a/src/renderer/locales/zh-Hant.json b/src/renderer/locales/zh-Hant.json index c5b09019..c50dcac7 100644 --- a/src/renderer/locales/zh-Hant.json +++ b/src/renderer/locales/zh-Hant.json @@ -568,13 +568,17 @@ "searchPlaceholder": "搜尋", "filterAll": "全部", "filterLocal": "本地音效", + "filterHidden": "隱藏的音效", "noSounds": "無音效", "loadFailed": "音效清單載入失敗", "edit": "編輯", "rename": "重新命名", + "hide": "隱藏", + "unhide": "取消隱藏", "delete": "刪除", "deleteFailed": "音效刪除失敗", "renameFailed": "音效重新命名失敗", + "hideFailed": "無法變更音效顯示狀態", "deleteConfirm": "確定刪除該音效?檔案將被刪除,並從所有使用它的元素中移除。", "add": "新增音效" }, diff --git a/src/renderer/locales/zh-cn.json b/src/renderer/locales/zh-cn.json index 72962a4f..be5b4d8a 100644 --- a/src/renderer/locales/zh-cn.json +++ b/src/renderer/locales/zh-cn.json @@ -568,13 +568,17 @@ "searchPlaceholder": "搜索", "filterAll": "全部", "filterLocal": "本地音频", + "filterHidden": "隐藏的音效", "noSounds": "无音频", "loadFailed": "音频列表加载失败", "edit": "编辑", "rename": "重命名", + "hide": "隐藏", + "unhide": "取消隐藏", "delete": "删除", "deleteFailed": "音频删除失败", "renameFailed": "音频重命名失败", + "hideFailed": "无法更改音效显示状态", "deleteConfirm": "确定删除该音频?文件将被删除,并从所有使用它的元素中移除。", "add": "添加音效" }, diff --git a/src/renderer/styles/tokens.css b/src/renderer/styles/tokens.css index 0f730e5f..c68905d8 100644 --- a/src/renderer/styles/tokens.css +++ b/src/renderer/styles/tokens.css @@ -52,6 +52,13 @@ --ui-accent-deep-hover: #7857d8; --ui-accent-deep-active: #5f40bd; + /* 캔버스 선택 크롬 — 셀렉션은 의미 지점이므로 브랜드 바이올렛 */ + --ui-selection: #8b5cf6; + --ui-selection-border: rgba(139, 92, 246, 0.8); + --ui-selection-border-strong: rgba(139, 92, 246, 0.9); + --ui-selection-fill: rgba(139, 92, 246, 0.15); + --ui-selection-glow: rgba(139, 92, 246, 0.5); + /* 시맨틱 */ --ui-danger: #e5484d; --ui-danger-hover: #ec5d62; diff --git a/src/renderer/utils/grid/scrollShadow.ts b/src/renderer/utils/grid/scrollShadow.ts deleted file mode 100644 index 9ee67423..00000000 --- a/src/renderer/utils/grid/scrollShadow.ts +++ /dev/null @@ -1,90 +0,0 @@ -export interface ScrollShadowState { - hasTopShadow: boolean; - hasBottomShadow: boolean; -} - -const OVERFLOW_EPS = 0.5; // 스크롤 오버플로우 감지용 오차 -const CLIP_EPS = 1; // 경계에서 실제로 잘리는지 판단할 때 사용할 오차 -const OVERLAP_EPS = 0.5; // 요소가 뷰포트와 겹친다고 간주할 최소 오차 -const EDGE_EPS = 1; // 스크롤 상/하단 근접 판정 오차 - -/** - * Calculate whether scroll shadows should be shown based on whether - * any child element is currently clipped at the top or bottom edge - * of the scroll container. If there are no child elements, it falls - * back to the traditional scroll position checks. - */ -export function getScrollShadowState( - container: HTMLElement | null, - _content?: HTMLElement | null, -): ScrollShadowState { - if (!container) { - return { hasTopShadow: false, hasBottomShadow: false }; - } - - const hasOverflow = - container.scrollHeight - container.clientHeight > OVERFLOW_EPS; - if (!hasOverflow) { - return { hasTopShadow: false, hasBottomShadow: false }; - } - - const containerRect = container.getBoundingClientRect(); - - // 샘플 지점(컨테이너 내부) 계산 - const sampleX = - containerRect.left + - Math.min(Math.max(containerRect.width / 2, 2), containerRect.width - 2); - const sampleTopY = Math.min(containerRect.bottom - 2, containerRect.top + 2); - const sampleBottomY = Math.max( - containerRect.top + 2, - containerRect.bottom - 2, - ); - - const findElementAt = (y: number): HTMLElement | null => { - if (y < containerRect.top || y > containerRect.bottom) return null; - const hit = document.elementFromPoint(sampleX, y) as HTMLElement | null; - if (!hit) return null; - - let node: HTMLElement | null = hit; - while (node && node !== container && !container.contains(node)) { - node = node.parentElement; - } - - if (!node || node === document.documentElement || node === document.body) { - return null; - } - - return node; - }; - - const topTarget = findElementAt(sampleTopY); - const bottomTarget = findElementAt(sampleBottomY); - - const hasTopShadow = - !!topTarget && - (() => { - const rect = topTarget.getBoundingClientRect(); - const overlaps = - rect.bottom > containerRect.top + OVERLAP_EPS && - rect.top < containerRect.bottom - OVERLAP_EPS; - const clipped = rect.top < containerRect.top - CLIP_EPS; - const awayFromTop = container.scrollTop > EDGE_EPS; - return overlaps && clipped && awayFromTop; - })(); - - const hasBottomShadow = - !!bottomTarget && - (() => { - const rect = bottomTarget.getBoundingClientRect(); - const overlaps = - rect.top < containerRect.bottom - OVERLAP_EPS && - rect.bottom > containerRect.top + OVERLAP_EPS; - const clipped = rect.bottom > containerRect.bottom + CLIP_EPS; - const awayFromBottom = - container.scrollTop < - container.scrollHeight - container.clientHeight - EDGE_EPS; - return overlaps && clipped && awayFromBottom; - })(); - - return { hasTopShadow, hasBottomShadow }; -} diff --git a/src/types/plugin/api.ts b/src/types/plugin/api.ts index ed04c1e3..8e8cbc77 100644 --- a/src/types/plugin/api.ts +++ b/src/types/plugin/api.ts @@ -101,6 +101,8 @@ export type SoundListItem = { fileName: string; sizeBytes: number; modifiedAtMs?: number; + /** 피커 목록에서 숨김 여부 — 재생에는 영향 없음 */ + hidden: boolean; source: 'local' | 'builtin'; originalPath?: string; trimStartRatio?: number; @@ -108,6 +110,18 @@ export type SoundListItem = { displayName?: string; }; +export type SoundSetHiddenResult = { + success: boolean; + soundPath: string; + hidden: boolean; +}; + +export type SoundSetEnabledResult = { + success: boolean; + soundPath: string; + enabled: boolean; +}; + export type SoundSaveProcessedWavResult = { success: boolean; error?: string; @@ -881,6 +895,15 @@ export interface DMNoteAPI { list(): Promise; rename(soundPath: string, displayName: string): Promise; remove(soundPath: string): Promise; + setHidden( + soundPath: string, + hidden: boolean, + ): Promise; + /** @deprecated setHidden의 역논리 별칭 — enabled = !hidden */ + setEnabled( + soundPath: string, + enabled: boolean, + ): Promise; saveProcessedWav( wavBase64: string, fileName?: string, From 64cc94eee0e838428d84e4eea6778b7866f45422 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=97=B0=EC=9A=B0?= Date: Sat, 11 Jul 2026 20:53:26 +0900 Subject: [PATCH 03/17] =?UTF-8?q?fix:=20=ED=94=8C=EB=9F=AC=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EB=93=9C=EB=A1=AD=EB=8B=A4=EC=9A=B4=20Escape=20?= =?UTF-8?q?=EC=86=8C=EC=9C=A0=EA=B6=8C=20=EB=B0=8F=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=83=89=EC=9D=B8=20=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 플러그인 드롭다운 포털 메뉴가 Escape를 소비해 자신만 닫도록 하고, 네이티브 드롭다운과 같은 data-dmn-popup-submenu 표식 부여 (메뉴가 열린 채 Esc가 패널 페이지·그리드 선택 해제로 새는 문제 해소) - API 레퍼런스 개요의 네임스페이스 표에 dmn.sound 행 추가 (en/ko) --- docs/content/en/api-reference/index/page.mdx | 1 + docs/content/ko/api-reference/index/page.mdx | 1 + src/renderer/utils/plugin/pluginDropdownManager.ts | 13 +++++++++++++ 3 files changed, 15 insertions(+) diff --git a/docs/content/en/api-reference/index/page.mdx b/docs/content/en/api-reference/index/page.mdx index b1340683..9f7d76d7 100644 --- a/docs/content/en/api-reference/index/page.mdx +++ b/docs/content/en/api-reference/index/page.mdx @@ -18,6 +18,7 @@ All plugins access DM Note features through the `dmn` global object. | [`dmn.knobItems`](/docs/api-reference/knobs) | HID knob elements | | [`dmn.settings`](/docs/api-reference/settings) | App settings get/set | | [`dmn.overlay`](/docs/api-reference/overlay) | Overlay control | +| [`dmn.sound`](/docs/api-reference/sound) | Key sound library | | [`dmn.css`](/docs/api-reference/css-js#css) | CSS custom code | | [`dmn.js`](/docs/api-reference/css-js#js) | JS plugin management | | [`dmn.presets`](/docs/api-reference/presets) | Preset save/load | diff --git a/docs/content/ko/api-reference/index/page.mdx b/docs/content/ko/api-reference/index/page.mdx index 803099e5..ffb3017b 100644 --- a/docs/content/ko/api-reference/index/page.mdx +++ b/docs/content/ko/api-reference/index/page.mdx @@ -18,6 +18,7 @@ asIndexPage: true | [`dmn.knobItems`](/docs/api-reference/knobs) | HID 노브 요소 | | [`dmn.settings`](/docs/api-reference/settings) | 앱 설정 조회/변경 | | [`dmn.overlay`](/docs/api-reference/overlay) | 오버레이 제어 | +| [`dmn.sound`](/docs/api-reference/sound) | 키음 라이브러리 | | [`dmn.css`](/docs/api-reference/css-js#css) | CSS 커스텀 코드 | | [`dmn.js`](/docs/api-reference/css-js#js) | JS 플러그인 관리 | | [`dmn.presets`](/docs/api-reference/presets) | 프리셋 저장/로드 | diff --git a/src/renderer/utils/plugin/pluginDropdownManager.ts b/src/renderer/utils/plugin/pluginDropdownManager.ts index 76336ac5..efaae8e7 100644 --- a/src/renderer/utils/plugin/pluginDropdownManager.ts +++ b/src/renderer/utils/plugin/pluginDropdownManager.ts @@ -31,6 +31,7 @@ const resetMenuStyles = (menu: DropdownMenuElement) => { menu.style.boxShadow = ''; menu.style.borderRadius = ''; menu.dataset.pluginDropdownPortal = 'false'; + delete menu.dataset.dmnPopupSubmenu; }; export function setupPluginDropdownInteractions( @@ -124,6 +125,8 @@ export function setupPluginDropdownInteractions( menu.style.maxHeight = '200px'; menu.style.overflowY = 'auto'; menu.dataset.pluginDropdownPortal = 'true'; + // 네이티브 드롭다운과 같은 포털 메뉴 표식 — Escape 양보·클릭어웨이 예외 판정용 + menu.dataset.dmnPopupSubmenu = 'true'; openMenus.add(menu); @@ -209,7 +212,16 @@ export function setupPluginDropdownInteractions( } }; + // Escape 소유 — 최상위 포털 레이어이므로 소비 후 자신만 닫기 + const handleKeydown = (event: KeyboardEvent) => { + if (event.key !== 'Escape' || event.defaultPrevented) return; + if (!openMenus.size) return; + event.preventDefault(); + closeAllMenus(); + }; + document.addEventListener('click', handleClick, true); + document.addEventListener('keydown', handleKeydown); document.addEventListener('scroll', handleScrollOrResize, true); window.addEventListener('resize', handleScrollOrResize); @@ -221,6 +233,7 @@ export function setupPluginDropdownInteractions( closeAllMenus(); document.removeEventListener('click', handleClick, true); + document.removeEventListener('keydown', handleKeydown); document.removeEventListener('scroll', handleScrollOrResize, true); window.removeEventListener('resize', handleScrollOrResize); observer?.disconnect(); From b8409a5e37cfb65db23f0a6a682daf147fefd96f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=97=B0=EC=9A=B0?= Date: Sat, 11 Jul 2026 21:38:29 +0900 Subject: [PATCH 04/17] =?UTF-8?q?fix:=20=EC=82=AC=EC=9A=B4=EB=93=9C=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20enabled=20=EA=B3=84=EC=95=BD=20=EB=B3=B5?= =?UTF-8?q?=EA=B5=AC=20=EB=B0=8F=20=ED=94=BC=EC=BB=A4=C2=B7=EC=8A=A4?= =?UTF-8?q?=ED=86=A0=EC=96=B4=20=EC=95=88=EC=A0=95=EC=84=B1=20=EB=B3=B4?= =?UTF-8?q?=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SoundListItem 응답에 enabled(=!hidden)를 deprecated 필드로 병행 직렬화 — 1.6.1 플러그인 계약 유지 (Rust 응답·TS 타입·영/한 문서 동기화) - 카운터 애니메이션 피커 행의 키보드 타겟 가드 누락 수정 — ⋮ 버튼에 포커스 후 Enter가 메뉴 대신 프리셋 선택을 실행하던 문제. 키보드 포커스 시 ⋮ 버튼 노출(group-focus-within)도 보강 - store 저장을 원자 교체로 전환 — 임시 파일에 쓰고 fsync 후 rename. 쓰기 도중 크래시·전원 차단 시 설정 파일 손상 방지 - setLatencyLogging 문서에 dev/debug 빌드 제한 명시 (en/ko) --- docs/content/en/api-reference/sound/page.mdx | 5 ++++- docs/content/ko/api-reference/sound/page.mdx | 4 +++- src-tauri/src/commands/keys/sound.rs | 3 +++ src-tauri/src/state/store.rs | 20 +++++++++++++++++-- .../pickers/CounterAnimationPicker.tsx | 3 ++- src/types/plugin/api.ts | 2 ++ 6 files changed, 32 insertions(+), 5 deletions(-) diff --git a/docs/content/en/api-reference/sound/page.mdx b/docs/content/en/api-reference/sound/page.mdx index e1bbf251..30f2a980 100644 --- a/docs/content/en/api-reference/sound/page.mdx +++ b/docs/content/en/api-reference/sound/page.mdx @@ -18,6 +18,7 @@ type SoundListItem = { sizeBytes: number; modifiedAtMs?: number; hidden: boolean; // hidden from picker lists — playback is unaffected + enabled: boolean; // deprecated — inverse alias of hidden, kept for 1.6.1 compat source: 'local' | 'builtin'; originalPath?: string; // pre-trim original (present for edited sounds) trimStartRatio?: number; @@ -99,4 +100,6 @@ Overwrites an edited sound's processed WAV in place. ### `dmn.sound.setLatencyLogging(enabled): Promise` -Toggles key-sound latency logging in the audio engine. +Toggles key-sound latency logging in the audio engine. Enabling is only +available in dev/debug builds — release builds reject `enabled: true` with an +error. diff --git a/docs/content/ko/api-reference/sound/page.mdx b/docs/content/ko/api-reference/sound/page.mdx index 999dd295..b7a1be5e 100644 --- a/docs/content/ko/api-reference/sound/page.mdx +++ b/docs/content/ko/api-reference/sound/page.mdx @@ -18,6 +18,7 @@ type SoundListItem = { sizeBytes: number; modifiedAtMs?: number; hidden: boolean; // 피커 목록에서 숨김 — 재생에는 영향 없음 + enabled: boolean; // deprecated — hidden의 역논리 별칭, 1.6.1 호환 유지용 source: 'local' | 'builtin'; originalPath?: string; // 트림 전 원본 (편집된 사운드에 존재) trimStartRatio?: number; @@ -99,4 +100,5 @@ type SoundSetHiddenResult = { ### `dmn.sound.setLatencyLogging(enabled): Promise` -오디오 엔진의 키음 지연 로깅을 켜거나 끕니다. +오디오 엔진의 키음 지연 로깅을 켜거나 끕니다. 켜기는 dev/debug 빌드에서만 +가능하며, 릴리스 빌드에서 `enabled: true`는 에러로 거부됩니다. diff --git a/src-tauri/src/commands/keys/sound.rs b/src-tauri/src/commands/keys/sound.rs index f41544ea..50ad96bb 100644 --- a/src-tauri/src/commands/keys/sound.rs +++ b/src-tauri/src/commands/keys/sound.rs @@ -36,6 +36,8 @@ pub struct SoundListItem { #[serde(skip_serializing_if = "Option::is_none")] pub modified_at_ms: Option, pub hidden: bool, + // deprecated — 1.6.1 플러그인 계약 호환용 역논리 별칭 (enabled = !hidden) + pub enabled: bool, pub source: SoundSource, #[serde(skip_serializing_if = "Option::is_none")] pub original_path: Option, @@ -205,6 +207,7 @@ pub fn sound_list( size_bytes: metadata.len(), modified_at_ms, hidden: entry_meta.hidden, + enabled: !entry_meta.hidden, source: entry_meta.source, original_path: entry_meta.original_path, trim_start_ratio: entry_meta.trim_start_ratio, diff --git a/src-tauri/src/state/store.rs b/src-tauri/src/state/store.rs index 57f23f29..35feb4a7 100644 --- a/src-tauri/src/state/store.rs +++ b/src-tauri/src/state/store.rs @@ -1,6 +1,7 @@ use std::{ collections::HashSet, fs, + io::Write, path::{Path, PathBuf}, }; @@ -257,8 +258,23 @@ impl AppStore { } let json = serde_json::to_string_pretty(&root)?; - fs::write(&self.path, json) - .with_context(|| format!("failed to write store file at {}", self.path.display())) + // 원자 교체 — 쓰기 도중 크래시해도 기존 store가 온전히 남도록 + // 같은 디렉토리의 임시 파일에 쓴 뒤 rename (동일 파일시스템 보장) + let tmp_path = self.path.with_extension("json.tmp"); + { + let mut tmp = fs::File::create(&tmp_path).with_context(|| { + format!("failed to create temp store file at {}", tmp_path.display()) + })?; + tmp.write_all(json.as_bytes()).with_context(|| { + format!("failed to write temp store file at {}", tmp_path.display()) + })?; + // 전원 손실 대비 — rename 전에 데이터를 디스크로 플러시 + tmp.sync_all().with_context(|| { + format!("failed to sync temp store file at {}", tmp_path.display()) + })?; + } + fs::rename(&tmp_path, &self.path) + .with_context(|| format!("failed to replace store file at {}", self.path.display())) } /// 앱 종료 시점에 한 번 호출하는 자원 정리. diff --git a/src/renderer/components/main/Modal/content/pickers/CounterAnimationPicker.tsx b/src/renderer/components/main/Modal/content/pickers/CounterAnimationPicker.tsx index d058d9b4..3ce9246c 100644 --- a/src/renderer/components/main/Modal/content/pickers/CounterAnimationPicker.tsx +++ b/src/renderer/components/main/Modal/content/pickers/CounterAnimationPicker.tsx @@ -259,6 +259,7 @@ const CounterAnimationPicker = ({ tabIndex={0} onClick={() => handlePresetSelect(preset)} onKeyDown={(event) => { + if (event.target !== event.currentTarget) return; if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); handlePresetSelect(preset); @@ -281,7 +282,7 @@ const CounterAnimationPicker = ({ className={`${pickerMoreButtonClass} ${ isSelected || menu.menuKey === preset.id ? 'opacity-100' - : 'opacity-0 group-hover:opacity-100' + : 'opacity-0 group-hover:opacity-100 group-focus-within:opacity-100' } ${ isSelected ? 'text-fg hover:text-fg' diff --git a/src/types/plugin/api.ts b/src/types/plugin/api.ts index 8e8cbc77..2e485599 100644 --- a/src/types/plugin/api.ts +++ b/src/types/plugin/api.ts @@ -103,6 +103,8 @@ export type SoundListItem = { modifiedAtMs?: number; /** 피커 목록에서 숨김 여부 — 재생에는 영향 없음 */ hidden: boolean; + /** @deprecated hidden의 역논리 별칭 (enabled = !hidden) — 1.6.1 호환 */ + enabled: boolean; source: 'local' | 'builtin'; originalPath?: string; trimStartRatio?: number; From af1defc8145896bc9edfd6ab39f692561233df15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=97=B0=EC=9A=B0?= Date: Sun, 12 Jul 2026 00:30:00 +0900 Subject: [PATCH 05/17] =?UTF-8?q?feat:=20=ED=8C=A8=EB=84=90=20=ED=97=A4?= =?UTF-8?q?=EB=8D=94=2048px=20=EC=95=B5=EC=BB=A4=20=ED=86=B5=EC=9D=BC=20?= =?UTF-8?q?=EB=B0=8F=20=EC=95=84=EC=9D=B4=EC=BD=98=20=EC=A0=84=ED=99=98=20?= =?UTF-8?q?=EB=AA=A8=EC=85=98=20=EB=8F=84=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사이드 패널 헤더 9곳을 PANEL_HEADER_CLASS(h-48px)로 통일하고 버튼 네거티브 마진 핵 제거 — 헤더 버튼 중심이 우상단 (24,24) 앵커에 고정되어 패널 열림/닫힘 간 토글 아이콘 위치 일치. 피커 서브 페이지 헤더(CommonListPickerPage)도 같은 높이로 동기 - 패널 열기/닫기 버튼을 PanelToggleButton 단일 지속 노드로 통합 — 리마운트로 인한 hover 끊김·깜빡임 제거. 글래스 칩은 opacity 페이드(WAAPI), 아이콘 구분선 슬라이드·디테일 라인 페이드는 CSS. 블러+opacity 조합은 WKWebView에서 블러 레이어가 점멸해 칩 재질을 블러 없는 glass-panel로 교체 - 헤더 우측 액션을 PanelHeaderActions로 승격해 루트 페이지 레이어에서 지속 렌더 — 패널 본문 교체 시에도 모드 전환 버튼 노드 유지, 전환 대상 모드 아이콘이 크로스페이드로 스왑 - IconSwap 공용 컴포넌트 추가 (겹친 두 SVG 블러+스케일 크로스페이드, 다이얼은 --ui-icon-swap-* 토큰) — 툴바 설정/뒤로, 레이어 패널 그룹·개별 눈, 사운드 트림 재생/일시정지에 적용 - EyeToggleIcon 추가 — 오버레이 표시 토글의 사선을 마스크 갭과 함께 대각선 슬라이드로 드로우/이레이즈 (한 방향 관통 스와이프). WAAPI FROM 고정 + onfinish 커밋 패턴으로 타이머 경주 제거 - 미사용 SidebarToggleIcon 및 패널별 토글 핸들러 props 정리 --- .../components/main/Grid/PropertiesPanel.tsx | 126 +++++++++--------- .../PropertiesPanel/PanelHeaderActions.tsx | 58 ++++++++ .../PropertiesPanel/PanelToggleButton.tsx | 124 +++++++++++++++++ .../PluginSettingsPanelView.tsx | 13 +- .../Grid/PropertiesPanel/PropertyInputs.tsx | 57 -------- .../batch/BatchSelectionPanel.tsx | 88 +----------- .../main/Grid/PropertiesPanel/index.ts | 1 - .../Grid/PropertiesPanel/layer/LayerPanel.tsx | 38 +----- .../PropertiesPanel/layer/LayerTabContent.tsx | 57 +++++--- .../main/Grid/PropertiesPanel/panelChrome.ts | 5 + .../single/SingleSelectionPanel.tsx | 111 ++------------- .../Modal/content/managers/SoundTrimModal.tsx | 69 ++++++---- .../content/pickers/CommonListPickerPage.tsx | 3 +- .../components/main/Tool/SettingTool.tsx | 14 +- .../components/main/common/EyeToggleIcon.tsx | 119 +++++++++++++++++ .../components/main/common/IconSwap.tsx | 22 +++ src/renderer/styles/main.css | 71 ++++++++++ src/renderer/styles/tokens.css | 7 + 18 files changed, 580 insertions(+), 403 deletions(-) create mode 100644 src/renderer/components/main/Grid/PropertiesPanel/PanelHeaderActions.tsx create mode 100644 src/renderer/components/main/Grid/PropertiesPanel/PanelToggleButton.tsx create mode 100644 src/renderer/components/main/common/EyeToggleIcon.tsx create mode 100644 src/renderer/components/main/common/IconSwap.tsx diff --git a/src/renderer/components/main/Grid/PropertiesPanel.tsx b/src/renderer/components/main/Grid/PropertiesPanel.tsx index 2c5892ab..566af37e 100644 --- a/src/renderer/components/main/Grid/PropertiesPanel.tsx +++ b/src/renderer/components/main/Grid/PropertiesPanel.tsx @@ -41,7 +41,6 @@ import { ColorInput, TextInput, SectionDivider, - SidebarToggleIcon, LayerPanel, PluginSelectionPanel, SingleGraphPanel, @@ -56,6 +55,8 @@ import { } from './PropertiesPanel/index'; import { SIDE_PANEL_FRAME_CLASS } from './PropertiesPanel/panelChrome'; import { PanelNavProvider } from './PropertiesPanel/PanelNavContext'; +import PanelHeaderActions from './PropertiesPanel/PanelHeaderActions'; +import PanelToggleButton from './PropertiesPanel/PanelToggleButton'; import Checkbox from '@components/main/common/Checkbox'; import Dropdown from '@components/main/common/Dropdown'; import type { NoteColor } from '@src/types/key/keys'; @@ -1059,6 +1060,8 @@ const PropertiesPanel: React.FC = ({ setPanelMode((prev) => (prev === 'layer' ? 'property' : 'layer')); }; + const showFrame = isPanelVisible || !!pluginSettingsPanel; + const pluginDefaultSettings = (() => { const defaults: Record = {}; if (selectedPluginDefinition?.settings) { @@ -2580,22 +2583,6 @@ const PropertiesPanel: React.FC = ({ // 렌더링 // ============================================================================ - // 패널이 닫혀있을 때는 토글 버튼만 표시 - if (!isPanelVisible && !pluginSettingsPanel) { - return ( -
- -
- ); - } - // 선택 상태별 구상 패널 — 프레임(글래스) 안의 루트 페이지 콘텐츠 const renderPanelBody = () => { if (pluginSettingsPanel) { @@ -2616,13 +2603,9 @@ const PropertiesPanel: React.FC = ({ // 레이어 모드일 때는 선택 여부와 관계없이 레이어 패널 표시 if (panelMode === 'layer') { - const hasAnySelection = - selectedKeyElements.length > 0 || selectedElements.length > 0; return ( { selectionFromLayerPanelRef.current = true; }} @@ -2634,7 +2617,6 @@ const PropertiesPanel: React.FC = ({ if (selectedKeyElements.length === 0 && selectedElements.length === 0) { return ( { selectionFromLayerPanelRef.current = true; @@ -2667,8 +2649,6 @@ const PropertiesPanel: React.FC = ({ handleRenameCommit={handleRenameCommit} handleRenameCancel={handleRenameCancel} handleRenameStart={handleRenameStart} - handleToggleMode={handleToggleMode} - handleTogglePanel={handleTogglePanel} activeTab={activeTab} setActiveTab={setActiveTab} handleBatchAlign={handleBatchAlign} @@ -2768,8 +2748,6 @@ const PropertiesPanel: React.FC = ({ handleRenameCommit={handleRenameCommit} handleRenameCancel={handleRenameCancel} handleRenameStart={handleRenameStart} - handleToggleMode={handleToggleMode} - handleTogglePanel={handleTogglePanel} handleBatchAlign={handleBatchAlign} handleBatchDistribute={handleBatchDistribute} handleBatchSpacing={handleBatchSpacing} @@ -2815,8 +2793,6 @@ const PropertiesPanel: React.FC = ({ handleRenameCommit={handleRenameCommit} handleRenameCancel={handleRenameCancel} handleRenameStart={handleRenameStart} - handleToggleMode={handleToggleMode} - handleTogglePanel={handleTogglePanel} handleBatchAlign={handleBatchAlign} handleBatchDistribute={handleBatchDistribute} handleBatchSpacing={handleBatchSpacing} @@ -2858,8 +2834,6 @@ const PropertiesPanel: React.FC = ({ = ({ handleRenameCancel={handleRenameCancel} handleRenameStart={handleRenameStart} handleKnobUpdate={handleKnobUpdate} - handleToggleMode={handleToggleMode} - handleTogglePanel={handleTogglePanel} singleScrollRefFor={singleScrollRefFor} panelElement={panelElement} useCustomCSS={useCustomCSS} @@ -2934,8 +2906,6 @@ const PropertiesPanel: React.FC = ({ handleRenameCommit={handleRenameCommit} handleRenameCancel={handleRenameCancel} handleRenameStart={handleRenameStart} - handleToggleMode={handleToggleMode} - handleTogglePanel={handleTogglePanel} handleGraphUpdate={handleGraphUpdate} singleScrollRefFor={singleScrollRefFor} showGraphImagePicker={showGraphImagePicker} @@ -2977,8 +2947,6 @@ const PropertiesPanel: React.FC = ({ handleRenameCommit={handleRenameCommit} handleRenameCancel={handleRenameCancel} handleRenameStart={handleRenameStart} - handleToggleMode={handleToggleMode} - handleTogglePanel={handleTogglePanel} activeTab={activeTab} setActiveTab={setActiveTab} onPositionChange={onPositionChange} @@ -3003,33 +2971,71 @@ const PropertiesPanel: React.FC = ({ ); }; - const panelBody = renderPanelBody(); - if (!panelBody) return null; + // 열림/닫힘과 무관하게 항상 렌더되는 지속 토글 — 리마운트 없이 모프 전환 + const toggleButton = ( + + ); + + const panelBody = showFrame ? renderPanelBody() : null; + + // 헤더 액션 기준 모드 — panelMode가 property여도 선택이 없으면 레이어 뷰가 표시됨 + const hasAnySelection = + selectedKeyElements.length > 0 || selectedElements.length > 0; + const displayedPanelMode = + panelMode === 'layer' || !hasAnySelection ? 'layer' : 'property'; - // 프레임이 글래스를 소유하고, 루트/서브 페이지가 그 안에서 슬라이드 전환 + // 프레임이 글래스를 소유하고, 루트/서브 페이지가 그 안에서 슬라이드 전환. + // 열림/닫힘 모두 같은 프래그먼트 구조 유지 — 토글 버튼이 리마운트되면 + // 호버 상태가 끊겨 아이콘이 깜빡임 return ( - -
- {/* inert — 슬라이드 아웃된 레이어를 키보드 탭 순회·접근성 트리에서 제외 */} -
+ {showFrame && panelBody && ( + - {panelBody} -
-
-
- +
+ {/* inert — 슬라이드 아웃된 레이어를 키보드 탭 순회·접근성 트리에서 제외 */} +
+ {panelBody} + +
+
+
+ + )} + {toggleButton} + ); }; diff --git a/src/renderer/components/main/Grid/PropertiesPanel/PanelHeaderActions.tsx b/src/renderer/components/main/Grid/PropertiesPanel/PanelHeaderActions.tsx new file mode 100644 index 00000000..63588f86 --- /dev/null +++ b/src/renderer/components/main/Grid/PropertiesPanel/PanelHeaderActions.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { useTranslation } from '@contexts/useTranslation'; +import IconSwap from '@components/main/common/IconSwap'; +import { ModeToggleIcon } from './PropertyInputs'; + +interface PanelHeaderActionsProps { + // 실제 표시 중인 패널 기준 모드 (panelMode가 property여도 선택이 없으면 layer 뷰) + mode: 'layer' | 'property'; + // 플러그인 설정 패널에서는 모드 전환 숨김 + modeToggleHidden?: boolean; + modeToggleDisabled?: boolean; + onToggleMode: () => void; +} + +// 패널 헤더 우측 액션 — 패널 본문(루트 페이지 콘텐츠)이 통째로 교체되어도 +// 버튼 노드가 유지되도록 프레임의 루트 페이지 레이어에서 렌더. +// 아이콘 스왑 전환이 발동하고, 리마운트로 인한 hover 깜빡임도 없음. +// 닫기 토글은 프레임 밖의 PanelToggleButton이 담당 — 오른쪽 40px 비워둠 +const PanelHeaderActions = ({ + mode, + modeToggleHidden = false, + modeToggleDisabled = false, + onToggleMode, +}: PanelHeaderActionsProps) => { + const { t } = useTranslation(); + const modeToggleLabel = + mode === 'property' + ? t('propertiesPanel.switchToLayer') || 'Switch to Layer' + : t('propertiesPanel.switchToProperty') || 'Switch to Property'; + + if (modeToggleHidden) return null; + + return ( +
+ +
+ ); +}; + +export default PanelHeaderActions; diff --git a/src/renderer/components/main/Grid/PropertiesPanel/PanelToggleButton.tsx b/src/renderer/components/main/Grid/PropertiesPanel/PanelToggleButton.tsx new file mode 100644 index 00000000..c1d4bfc0 --- /dev/null +++ b/src/renderer/components/main/Grid/PropertiesPanel/PanelToggleButton.tsx @@ -0,0 +1,124 @@ +import React, { useLayoutEffect, useRef } from 'react'; +import { useTranslation } from '@contexts/useTranslation'; + +interface PanelToggleButtonProps { + open: boolean; + onClick: () => void; +} + +// tokens.css의 --ui-duration-base / --ui-ease-out과 동기 (WAAPI는 CSS 변수 참조 불가) +const FADE_MS = 180; +const FADE_EASE = 'cubic-bezier(0.16, 1, 0.3, 1)'; + +const CHIP_SHOWN = '1'; +const CHIP_HIDDEN = '0'; + +// 패널 열기/닫기 토글 — 열림/닫힘을 하나의 지속 노드로 렌더해 +// 리마운트 깜빡임 없이 글래스 칩 ↔ 베어 아이콘으로 모프. +// 아이콘 중심은 두 상태 모두 우상단 (24, 24) 앵커에 고정. +// 칩은 opacity 페이드 — backdrop-filter가 없는 재질이라 안전 +// (블러+opacity 조합은 WKWebView에서 블러 레이어가 점멸). +// 눈 토글과 같은 규칙: FROM 상태를 정적으로 고정하고 TO 커밋은 onfinish에서 +const PanelToggleButton = ({ open, onClick }: PanelToggleButtonProps) => { + const { t } = useTranslation(); + const chipRef = useRef(null); + const mountedRef = useRef(false); + const animRef = useRef(null); + + useLayoutEffect(() => { + const chip = chipRef.current; + if (!chip) return; + + // 칩은 opacity로만 움직임 — 잔류 인라인 transform 방어적 제거 + chip.style.transform = ''; + + // 초기 마운트는 정적 상태 그대로 — 페이드 연출 없음 + const restState = open ? CHIP_HIDDEN : CHIP_SHOWN; + if (!mountedRef.current) { + mountedRef.current = true; + chip.style.opacity = restState; + return; + } + + animRef.current?.cancel(); + animRef.current = null; + + if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { + chip.style.opacity = restState; + return; + } + + const from = open ? CHIP_SHOWN : CHIP_HIDDEN; + chip.style.opacity = from; + const anim = chip.animate([{ opacity: from }, { opacity: restState }], { + duration: FADE_MS, + easing: FADE_EASE, + }); + anim.onfinish = () => { + chip.style.opacity = restState; + }; + animRef.current = anim; + }, [open]); + + const label = open + ? t('propertiesPanel.closePanel') || '속성 패널 닫기' + : t('propertiesPanel.openPanel') || '속성 패널 열기'; + + return ( +
+ +
+ ); +}; + +export default PanelToggleButton; diff --git a/src/renderer/components/main/Grid/PropertiesPanel/PluginSettingsPanelView.tsx b/src/renderer/components/main/Grid/PropertiesPanel/PluginSettingsPanelView.tsx index 6500b55a..8931f98a 100644 --- a/src/renderer/components/main/Grid/PropertiesPanel/PluginSettingsPanelView.tsx +++ b/src/renderer/components/main/Grid/PropertiesPanel/PluginSettingsPanelView.tsx @@ -5,8 +5,7 @@ import type { PluginSettingValue, } from '@src/types/plugin/api'; import type { PluginSettingsPanelPayload } from '@stores/grid/usePropertiesPanelStore'; -import { SidebarToggleIcon } from './PropertyInputs'; -import { PANEL_ROOT_CLASS } from './panelChrome'; +import { PANEL_ROOT_CLASS, PANEL_HEADER_CLASS } from './panelChrome'; interface PluginSettingsPanelViewProps { setPanelElement: (el: HTMLDivElement | null) => void; @@ -43,7 +42,7 @@ const PluginSettingsPanelView: React.FC = ({ }) => { return (
-
+
{t('propertiesPanel.pluginSettings') || '플러그인 설정'} @@ -52,14 +51,6 @@ const PluginSettingsPanelView: React.FC = ({ {pluginSettingsPanel.pluginId}
-
( ); -export const SidebarToggleIcon: React.FC<{ - isOpen: boolean; - color?: string; -}> = ({ isOpen, color = 'currentColor' }) => ( - - - - {isOpen && ( - <> - - - - - )} - -); - // 레이어/속성 모드 전환 토글 아이콘 export const ModeToggleIcon: React.FC<{ mode: 'layer' | 'property'; diff --git a/src/renderer/components/main/Grid/PropertiesPanel/batch/BatchSelectionPanel.tsx b/src/renderer/components/main/Grid/PropertiesPanel/batch/BatchSelectionPanel.tsx index afe1e3c5..ac905546 100644 --- a/src/renderer/components/main/Grid/PropertiesPanel/batch/BatchSelectionPanel.tsx +++ b/src/renderer/components/main/Grid/PropertiesPanel/batch/BatchSelectionPanel.tsx @@ -11,7 +11,7 @@ import type { } from '@src/types/key/graphItems'; import type { KnobItemPosition } from '@src/types/key/knobs'; import type { SelectedElement } from '@stores/grid/useGridSelectionStore'; -import { PANEL_ROOT_CLASS } from '../panelChrome'; +import { PANEL_ROOT_CLASS, PANEL_HEADER_CLASS } from '../panelChrome'; import { normalizeCounterSettings, createDefaultCounterSettings, @@ -20,8 +20,6 @@ import { PropertyRow, NumberInput, ColorInput, - SidebarToggleIcon, - ModeToggleIcon, Tabs, BatchStyleTabContent, BatchNoteTabContent, @@ -111,8 +109,6 @@ interface BatchKeyLikePanelProps { handleRenameCommit: (value: string) => void; handleRenameCancel: () => void; handleRenameStart: () => void; - handleToggleMode: () => void; - handleTogglePanel: () => void; activeTab: TabType; setActiveTab: (tab: TabType) => void; // batch handlers @@ -214,8 +210,6 @@ export const BatchKeyLikePanel: React.FC = ({ handleRenameCommit, handleRenameCancel, handleRenameStart, - handleToggleMode, - handleTogglePanel, activeTab, setActiveTab, handleBatchAlign, @@ -525,7 +519,7 @@ export const BatchKeyLikePanel: React.FC = ({ {/* 헤더 + 탭 영역 */}
{/* 헤더 */} -
+
{selectedGroupInfo ? ( isRenaming ? ( @@ -562,7 +556,7 @@ export const BatchKeyLikePanel: React.FC = ({
-
- - -
{/* 탭 */} @@ -976,8 +950,6 @@ interface BatchGraphOnlyPanelProps { handleRenameCommit: (value: string) => void; handleRenameCancel: () => void; handleRenameStart: () => void; - handleToggleMode: () => void; - handleTogglePanel: () => void; handleBatchAlign: ( direction: 'left' | 'centerH' | 'right' | 'top' | 'centerV' | 'bottom', ) => void; @@ -1024,8 +996,6 @@ export const BatchGraphOnlyPanel: React.FC = ({ handleRenameCommit, handleRenameCancel, handleRenameStart, - handleToggleMode, - handleTogglePanel, handleBatchAlign, handleBatchDistribute, handleBatchSpacing, @@ -1082,7 +1052,7 @@ export const BatchGraphOnlyPanel: React.FC = ({ return (
-
+
{selectedGroupInfo ? ( isRenaming ? ( @@ -1119,7 +1089,7 @@ export const BatchGraphOnlyPanel: React.FC = ({
-
- - -
@@ -1361,8 +1311,6 @@ interface BatchKnobOnlyPanelProps { handleRenameCommit: (value: string) => void; handleRenameCancel: () => void; handleRenameStart: () => void; - handleToggleMode: () => void; - handleTogglePanel: () => void; handleBatchAlign: ( direction: 'left' | 'centerH' | 'right' | 'top' | 'centerV' | 'bottom', ) => void; @@ -1409,8 +1357,6 @@ export const BatchKnobOnlyPanel: React.FC = ({ handleRenameCommit, handleRenameCancel, handleRenameStart, - handleToggleMode, - handleTogglePanel, handleBatchAlign, handleBatchDistribute, handleBatchSpacing, @@ -1442,7 +1388,7 @@ export const BatchKnobOnlyPanel: React.FC = ({ return (
-
+
{selectedGroupInfo ? ( isRenaming ? ( @@ -1479,7 +1425,7 @@ export const BatchKnobOnlyPanel: React.FC = ({
-
- - -
diff --git a/src/renderer/components/main/Grid/PropertiesPanel/index.ts b/src/renderer/components/main/Grid/PropertiesPanel/index.ts index c4bbc40c..8a6f7e14 100644 --- a/src/renderer/components/main/Grid/PropertiesPanel/index.ts +++ b/src/renderer/components/main/Grid/PropertiesPanel/index.ts @@ -14,7 +14,6 @@ export { Tabs, SectionDivider, CloseIcon, - SidebarToggleIcon, ModeToggleIcon, } from './PropertyInputs'; diff --git a/src/renderer/components/main/Grid/PropertiesPanel/layer/LayerPanel.tsx b/src/renderer/components/main/Grid/PropertiesPanel/layer/LayerPanel.tsx index d7064be5..ae75a11b 100644 --- a/src/renderer/components/main/Grid/PropertiesPanel/layer/LayerPanel.tsx +++ b/src/renderer/components/main/Grid/PropertiesPanel/layer/LayerPanel.tsx @@ -2,8 +2,7 @@ import React from 'react'; import { useTranslation } from '@contexts/useTranslation'; import { usePropertiesPanelStore } from '@stores/grid/usePropertiesPanelStore'; import { useGridSelectionStore } from '@stores/grid/useGridSelectionStore'; -import { SidebarToggleIcon, ModeToggleIcon } from '../PropertyInputs'; -import { PANEL_ROOT_CLASS } from '../panelChrome'; +import { PANEL_ROOT_CLASS, PANEL_HEADER_CLASS } from '../panelChrome'; import { LAYER_PANEL_TABS, type LayerPanelTabType } from '../types'; import LayerTabContent from './LayerTabContent'; import GridTabContent from '../GridTabContent'; @@ -13,9 +12,7 @@ import GridTabContent from '../GridTabContent'; // ============================================================================ interface LayerPanelProps { - onClose: () => void; onSwitchToProperty?: () => void; - hasSelection?: boolean; onSelectionFromPanel?: () => void; } @@ -92,9 +89,7 @@ const LayerPanelTabs: React.FC = ({ // ============================================================================ const LayerPanel: React.FC = ({ - onClose, onSwitchToProperty, - hasSelection = false, onSelectionFromPanel, }) => { const { t } = useTranslation(); @@ -123,39 +118,10 @@ const LayerPanel: React.FC = ({ {/* 헤더 + 탭 영역 */}
{/* 헤더 */} -
+
{t('propertiesPanel.canvas') || 'Canvas'} -
- {/* 모드 토글 버튼 - hasSelection에 따라 활성/비활성 */} - - {/* 패널 닫기 버튼 */} - -
{/* 탭 */} diff --git a/src/renderer/components/main/Grid/PropertiesPanel/layer/LayerTabContent.tsx b/src/renderer/components/main/Grid/PropertiesPanel/layer/LayerTabContent.tsx index 0fbd9796..15f306b3 100644 --- a/src/renderer/components/main/Grid/PropertiesPanel/layer/LayerTabContent.tsx +++ b/src/renderer/components/main/Grid/PropertiesPanel/layer/LayerTabContent.tsx @@ -13,6 +13,7 @@ import { usePluginDisplayElementStore } from '@stores/plugin/usePluginDisplayEle import { isMac } from '@utils/core/platform'; import { useLenis } from '@hooks/useLenis'; import ListPopup from '@components/main/Modal/ListPopup'; +import IconSwap from '@components/main/common/IconSwap'; import CloseEyeIcon from '@assets/svgs/close_eye.svg'; import OpenEyeIcon from '@assets/svgs/open_eye.svg'; import { useLayerGroupStore } from '@stores/data/useLayerGroupStore'; @@ -618,19 +619,23 @@ const LayerTabContent: React.FC = ({ : 'opacity-0 group-hover:opacity-60 hover:!opacity-100' }`} > - {gh.allHidden ? ( - - ) : ( - - )} + + } + inactiveIcon={ + + } + />
); @@ -756,15 +761,23 @@ const LayerTabContent: React.FC = ({ : 'opacity-0 group-hover:opacity-60 hover:!opacity-100' }`} > - {item.hidden ? ( - - ) : ( - - )} + + } + inactiveIcon={ + + } + />
); diff --git a/src/renderer/components/main/Grid/PropertiesPanel/panelChrome.ts b/src/renderer/components/main/Grid/PropertiesPanel/panelChrome.ts index eca4489c..4ac66b91 100644 --- a/src/renderer/components/main/Grid/PropertiesPanel/panelChrome.ts +++ b/src/renderer/components/main/Grid/PropertiesPanel/panelChrome.ts @@ -5,3 +5,8 @@ export const SIDE_PANEL_FRAME_CLASS = // 구상 패널 루트 — 프레임을 꽉 채우는 투명 레이어 export const PANEL_ROOT_CLASS = 'absolute inset-0 flex flex-col'; + +// 패널 헤더 — 48px 고정: 버튼 중심이 우상단 (24, 24) 앵커에 와서 +// PanelToggleButton(48px 컨테이너 센터링)의 아이콘 중심과 같은 지점에 고정 +export const PANEL_HEADER_CLASS = + 'flex items-center justify-between h-[48px] px-[12px] shrink-0'; diff --git a/src/renderer/components/main/Grid/PropertiesPanel/single/SingleSelectionPanel.tsx b/src/renderer/components/main/Grid/PropertiesPanel/single/SingleSelectionPanel.tsx index 35040ac7..e82b20e3 100644 --- a/src/renderer/components/main/Grid/PropertiesPanel/single/SingleSelectionPanel.tsx +++ b/src/renderer/components/main/Grid/PropertiesPanel/single/SingleSelectionPanel.tsx @@ -15,15 +15,13 @@ import type { PluginDisplayElementInternal, } from '@src/types/plugin/api'; import type { KeyInfo } from '@utils/core/KeyMaps'; -import { PANEL_ROOT_CLASS } from '../panelChrome'; +import { PANEL_ROOT_CLASS, PANEL_HEADER_CLASS } from '../panelChrome'; import { PropertyRow, NumberInput, TextInput, ColorInput, PropertySection, - SidebarToggleIcon, - ModeToggleIcon, Tabs, StyleTabContent, NoteTabContent, @@ -82,8 +80,6 @@ const RenameIcon: React.FC = () => ( interface PluginSelectionPanelProps { setPanelElement: (el: HTMLDivElement | null) => void; pluginTitle: string; - handleToggleMode: () => void; - handleTogglePanel: () => void; setPluginScrollRef: (node: HTMLDivElement | null) => void; isPluginResizable: boolean; selectedPluginElement: PluginDisplayElementInternal | null; @@ -112,8 +108,6 @@ interface PluginSelectionPanelProps { export const PluginSelectionPanel: React.FC = ({ setPanelElement, pluginTitle, - handleToggleMode, - handleTogglePanel, setPluginScrollRef, isPluginResizable, selectedPluginElement, @@ -133,28 +127,10 @@ export const PluginSelectionPanel: React.FC = ({ }) => { return (
-
+
{pluginTitle} -
- - -
void; handleRenameCancel: () => void; handleRenameStart: () => void; - handleToggleMode: () => void; - handleTogglePanel: () => void; handleGraphUpdate: ( data: Partial & { index: number }, ) => void; @@ -285,8 +259,6 @@ export const SingleGraphPanel: React.FC = ({ handleRenameCommit, handleRenameCancel, handleRenameStart, - handleToggleMode, - handleTogglePanel, handleGraphUpdate, singleScrollRefFor, showGraphImagePicker, @@ -310,7 +282,7 @@ export const SingleGraphPanel: React.FC = ({ return (
-
+
{isRenaming ? ( = ({
)} -
- - -
& { index: number }, ) => void; - handleToggleMode: () => void; - handleTogglePanel: () => void; singleScrollRefFor: (tab: TabType) => (node: HTMLDivElement | null) => void; panelElement: HTMLDivElement | null; useCustomCSS: boolean; @@ -771,8 +723,6 @@ export const SingleKnobPanel: React.FC = ({ handleRenameCancel, handleRenameStart, handleKnobUpdate, - handleToggleMode, - handleTogglePanel, singleScrollRefFor, panelElement, useCustomCSS, @@ -951,7 +901,7 @@ export const SingleKnobPanel: React.FC = ({ return (
-
+
{isRenaming ? ( = ({
)} -
- - -
@@ -1341,8 +1273,6 @@ interface SingleKeyStatPanelProps { handleRenameCommit: (value: string) => void; handleRenameCancel: () => void; handleRenameStart: () => void; - handleToggleMode: () => void; - handleTogglePanel: () => void; activeTab: TabType; setActiveTab: (tab: TabType) => void; onPositionChange: (index: number, dx: number, dy: number) => void; @@ -1391,8 +1321,6 @@ export const SingleKeyStatPanel: React.FC = ({ handleRenameCommit, handleRenameCancel, handleRenameStart, - handleToggleMode, - handleTogglePanel, activeTab, setActiveTab, onPositionChange, @@ -1511,7 +1439,7 @@ export const SingleKeyStatPanel: React.FC = ({ {/* 헤더 + 탭 영역 */}
{/* 헤더 */} -
+
{isRenaming ? ( = ({
)} - -
- {/* 레이어 모드로 전환 버튼 */} - - {/* 패널 닫기 버튼 */} - -
{/* 탭 */} diff --git a/src/renderer/components/main/Modal/content/managers/SoundTrimModal.tsx b/src/renderer/components/main/Modal/content/managers/SoundTrimModal.tsx index 90308432..2919b493 100644 --- a/src/renderer/components/main/Modal/content/managers/SoundTrimModal.tsx +++ b/src/renderer/components/main/Modal/content/managers/SoundTrimModal.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useRef, useState } from 'react'; import { useTranslation } from '@contexts/useTranslation'; import Modal from '../../Modal'; +import IconSwap from '@components/main/common/IconSwap'; import { getCursor, setCustomCursorHover, @@ -1066,33 +1067,47 @@ const SoundTrimModal = ({ onClick={handlePlay} disabled={!audioBuffer} > - {isPlaying ? ( - - - - - ) : ( - - - - )} + + + + + } + inactiveIcon={ + + + + } + /> {audioBuffer ? formatSecLabel(trimDurationMs) : '--'} diff --git a/src/renderer/components/main/Modal/content/pickers/CommonListPickerPage.tsx b/src/renderer/components/main/Modal/content/pickers/CommonListPickerPage.tsx index 8dd45df8..0c87b6df 100644 --- a/src/renderer/components/main/Modal/content/pickers/CommonListPickerPage.tsx +++ b/src/renderer/components/main/Modal/content/pickers/CommonListPickerPage.tsx @@ -64,7 +64,8 @@ export default function CommonListPickerPage({ return (
{/* 헤더 — 뒤로가기: 타이틀 캡하이트급 셰브론 + 타이틀 통짜 버튼 */} -
+ {/* 48px 고정 — 패널 루트 헤더(PANEL_HEADER_CLASS)와 높이 동기, 전환 시 타이틀 세로 고정 */} +
- -
+ {/* 단일 저장 CTA — 취소는 우상단 패널 토글(X)이 담당 */} +
+
); From cba7a68a7133a14625fa50b60dac490abab8d3b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=97=B0=EC=9A=B0?= Date: Sun, 12 Jul 2026 02:24:33 +0900 Subject: [PATCH 07/17] =?UTF-8?q?fix:=20=EB=AA=A8=EB=8B=AC=20=EB=93=B1?= =?UTF-8?q?=EC=9E=A5=20=EC=8B=9C=20=EA=B8=80=EB=9E=98=EC=8A=A4=20=EB=B8=94?= =?UTF-8?q?=EB=9F=AC=EA=B0=80=20=EC=A3=BD=EC=97=88=EB=8B=A4=20=ED=8A=80?= =?UTF-8?q?=EB=8A=94=20=EA=B9=9C=EB=B9=A1=EC=9E=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit opacity를 애니메이션하는 래퍼가 backdrop root가 되어 페이드 동안 자손 글래스의 backdrop-filter가 배경을 샘플링하지 못하고, 종료 순간 블러가 한 번에 복구되며 반투명 깜빡임으로 보였다. 등장 애니메이션을 래퍼 대신 콘텐츠 루트(> *)로 옮겨 블러를 소유한 엘리먼트 자신이 페이드하도록 변경 --- src/renderer/components/main/Modal/Modal.tsx | 5 +++-- src/renderer/styles/main.css | 15 +++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/renderer/components/main/Modal/Modal.tsx b/src/renderer/components/main/Modal/Modal.tsx index a936ef4e..ec752f52 100644 --- a/src/renderer/components/main/Modal/Modal.tsx +++ b/src/renderer/components/main/Modal/Modal.tsx @@ -45,9 +45,10 @@ const Modal = ({ onClick, children, animate = true }: ModalProps) => { // 배경 블러는 모달의 조상이 아니라 형제 언더레이가 소유해야 함 — // 조상이 backdrop-filter를 가지면 backdrop root가 생겨 모달 글래스의 // 블러가 WebKit에서 배경을 샘플링하지 못함. 형제 레이어는 정상 합성됨 - // 래퍼 opacity 애니메이션도 같은 이유로 금지 — opacity < 1인 조상이 + // 조상 opacity 애니메이션도 같은 이유로 금지 — opacity < 1인 조상이 // backdrop root가 되어 페이드 동안 블러가 죽었다가 끝나는 순간 튐. - // 등장 모션은 스크림(틴트+블러 키프레임)과 콘텐츠가 각자 소유한다 + // 그래서 animate-modal-scale은 래퍼가 아닌 직계 자식(> *)에 적용되고, + // 등장 모션은 스크림(틴트+블러 키프레임)과 콘텐츠 루트가 각자 소유한다 return createPortal(
* { + animation: modalScale var(--ui-duration-base) var(--ui-ease-out) both; } /* 틴트와 블러를 한 키프레임에서 보간 — 종료 상태는 클래스 자연값 */ @@ -455,7 +454,7 @@ body.dmn-custom-cursor * { /* 모션 축소 설정 존중 — 이동은 제거, 표시 자체는 유지 */ @media (prefers-reduced-motion: reduce) { .tooltip-fade-in, - .animate-modal-scale, + .animate-modal-scale > *, .animate-modal-scrim, .view-enter, .view-fade { From df881bafbae3090c24bee9c758b8edb49cdedfe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=97=B0=EC=9A=B0?= Date: Sun, 12 Jul 2026 05:17:50 +0900 Subject: [PATCH 08/17] =?UTF-8?q?feat:=20=EB=9D=BC=EC=9A=B4=EB=94=A9=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=204=EB=8B=A8=20=ED=99=95=EC=9E=A5=20?= =?UTF-8?q?=EB=B0=8F=20=EC=B9=B4=EB=93=9C=20=EB=A0=88=EC=8B=9C=ED=94=BC=20?= =?UTF-8?q?=EA=B3=B5=EC=9C=A0=20=EB=AA=A8=EB=93=88=20=EB=8F=84=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 모달 셸 반경 14 → 22, 팝업 셸 14 신설 — 동심 규칙(바깥 반경 = 안쪽 반경 + 간격) 정착 - 섹션 카드·라벨·폼 행 클래스를 cardRecipes 단일 소스로 추출해 React 패널과 플러그인 HTML 생성기가 동일한 카드 구조를 공유 --- src/renderer/styles/tokens.css | 7 +++++-- src/renderer/utils/cardRecipes.ts | 20 ++++++++++++++++++++ tailwind.config.js | 26 +------------------------- 3 files changed, 26 insertions(+), 27 deletions(-) create mode 100644 src/renderer/utils/cardRecipes.ts diff --git a/src/renderer/styles/tokens.css b/src/renderer/styles/tokens.css index 8df42f7d..1af89a9e 100644 --- a/src/renderer/styles/tokens.css +++ b/src/renderer/styles/tokens.css @@ -72,10 +72,13 @@ --ui-warning: #f0b429; --ui-warning-muted: rgba(240, 180, 41, 0.14); - /* 라운딩 — 3단: 표면 안 요소 6 < 1차 표면 10 < 모달 셸 14 */ + /* 라운딩 — 4단: 표면 안 요소 6 < 1차 표면 10 < 팝업 셸 14 < 모달 셸 16 */ + /* 동심 규칙은 코너 밀착 중첩에만 적용 (팝업 14 = 6 + p-8), 컨트롤은 고정 티어 */ + /* 모달 16 = macOS 시스템 윈도우 크롬 실측(≈16pt) 매칭 — 창보다 둥글면 위계 역전 */ --ui-radius-inner: 6px; --ui-radius-surface: 10px; - --ui-radius-modal: 14px; + --ui-radius-popup: 14px; + --ui-radius-modal: 16px; /* 모션 */ --ui-duration-fast: 120ms; diff --git a/src/renderer/utils/cardRecipes.ts b/src/renderer/utils/cardRecipes.ts new file mode 100644 index 00000000..7037d333 --- /dev/null +++ b/src/renderer/utils/cardRecipes.ts @@ -0,0 +1,20 @@ +// 카드 그룹 시스템 단일 소스 — 패널(PropertyInputs)과 플러그인 문자열 빌더 +// (defineSettings/defineElement/pluginComponents)가 같은 클래스를 공유한다. +// 주의: Tailwind가 클래스를 감지하도록 항상 완전한 클래스 문자열로 유지할 것 + +// 섹션 래퍼 — 라벨 + 카드를 6px 간격으로 묶음 +export const SECTION_WRAPPER_CLASS = 'flex flex-col gap-[6px]'; + +// 섹션 라벨 — 카드 위 faint 서브헤더 +export const SECTION_LABEL_CLASS = 'text-fg-faint text-body text-left px-[2px]'; + +// 그룹 카드 — 관련 행을 하나의 면으로 묶는 컨테이너 +export const SECTION_CARD_CLASS = + 'bg-fill-faint rounded-surface px-[10px] py-[4px] flex flex-col'; + +// 폼 행 — 행이 수직 공간을 소유 (고정 min-h + 센터 정렬) +export const FORM_ROW_CLASS = + 'flex justify-between items-center w-full min-h-[32px]'; + +// 행 라벨 +export const FORM_LABEL_CLASS = 'text-fg-muted text-label'; diff --git a/tailwind.config.js b/tailwind.config.js index 578de8a0..dbdad48d 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -8,6 +8,7 @@ module.exports = { borderRadius: { md: 'var(--ui-radius-inner)', surface: 'var(--ui-radius-surface)', + popup: 'var(--ui-radius-popup)', modal: 'var(--ui-radius-modal)', }, colors: { @@ -132,31 +133,6 @@ module.exports = { letterSpacing: '-0.014em', fontWeight: '700', }, - // 레거시 별칭 — 마이그레이션 완료 후 제거 - '.text-style-1': { - fontSize: '12px', - lineHeight: '18px', - letterSpacing: '0', - fontWeight: '500', - }, - '.text-style-2': { - fontSize: '13px', - lineHeight: '18px', - letterSpacing: '0', - fontWeight: '500', - }, - '.text-style-3': { - fontSize: '14px', - lineHeight: '20px', - letterSpacing: '0', - fontWeight: '500', - }, - '.text-style-4': { - fontSize: '14px', - lineHeight: '20px', - letterSpacing: '0', - fontWeight: '500', - }, }); }, ], From c4f38f41eccb4e6886cf1ca382fccaf6b2780550 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=97=B0=EC=9A=B0?= Date: Sun, 12 Jul 2026 05:18:09 +0900 Subject: [PATCH 09/17] =?UTF-8?q?feat:=20=EB=AA=A8=EB=8B=AC=C2=B7=EB=8B=A4?= =?UTF-8?q?=EC=9D=B4=EC=96=BC=EB=A1=9C=EA=B7=B8=C2=B7=ED=94=BC=EC=BB=A4=20?= =?UTF-8?q?=EC=A7=80=EC=98=A4=EB=A9=94=ED=8A=B8=EB=A6=AC=20=EC=A0=95?= =?UTF-8?q?=EB=B9=84=20=EB=B0=8F=20=EC=A0=91=EA=B7=BC=EC=84=B1=20=EB=B3=B4?= =?UTF-8?q?=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 카드 기준 역산 지오메트리 적용: 모달 셸 p-12 + 반경 22, 피커류는 rounded-popup 유지 - NoteSetting을 카드 구조(PropertySection/PropertyRow)로 개편, 수직 리듬 12로 단일화 - Modal에 스크린리더 라벨(ariaLabel)과 닫힘 시 포커스 복원 추가, 로케일에 다이얼로그 라벨 키 보강 - 컨트롤 높이 23px로 통일(설정 칩·아이콘 버튼, 드롭다운 행·트리거), 라운딩을 rounded-surface/md 토큰으로 정리 - 카운터 애니메이션 에디터 스케일 입력 범위 명시(0-9999) - 실험실(Laboratory)과 legacy Counter/KeySetting 모달 제거 --- .../main/Modal/ManagerModalLayout.tsx | 18 +- src/renderer/components/main/Modal/Modal.tsx | 52 +- .../main/Modal/content/dialogs/Laboratory.tsx | 197 -------- .../content/dialogs/PluginDataDeleteModal.tsx | 31 +- .../content/dialogs/UnifiedKeySetting.tsx | 40 +- .../Modal/content/dialogs/UpdateModal.tsx | 43 +- .../editors/CounterAnimationEditorModal.tsx | 10 +- .../Modal/content/editors/TabCssModal.tsx | 36 +- .../Modal/content/editors/TabNameModal.tsx | 37 +- .../Modal/content/legacy/CounterSetting.tsx | 469 ------------------ .../main/Modal/content/legacy/KeySetting.tsx | 351 ------------- .../content/managers/PluginManagerModal.tsx | 7 +- .../Modal/content/managers/SoundTrimModal.tsx | 8 +- .../Modal/content/pickers/ColorPicker.tsx | 18 +- .../content/pickers/CommonListPickerPage.tsx | 2 +- .../pickers/CounterAnimationPicker.tsx | 11 +- .../main/Modal/content/pickers/FontPicker.tsx | 17 +- .../Modal/content/pickers/ImagePicker.tsx | 2 +- .../main/Modal/content/pickers/Palette.tsx | 4 +- .../Modal/content/pickers/SoundPicker.tsx | 13 +- .../content/pickers/WebFontInputModal.tsx | 11 +- .../Modal/content/pickers/pickerRowClass.ts | 16 +- .../content/settings/CounterTabContent.tsx | 268 +++++----- .../Modal/content/settings/KeyTabContent.tsx | 233 ++++----- .../Modal/content/settings/NoteSetting.tsx | 318 ++++++------ .../Modal/content/settings/NoteTabContent.tsx | 165 +++--- .../settings/ShortcutSettingsModal.tsx | 10 +- src/renderer/components/main/Settings.tsx | 44 +- .../components/main/common/Dropdown.tsx | 10 +- src/renderer/locales/en.json | 3 +- src/renderer/locales/ko.json | 3 +- src/renderer/locales/ru.json | 3 +- src/renderer/locales/zh-Hant.json | 3 +- src/renderer/locales/zh-cn.json | 3 +- 34 files changed, 752 insertions(+), 1704 deletions(-) delete mode 100644 src/renderer/components/main/Modal/content/dialogs/Laboratory.tsx delete mode 100644 src/renderer/components/main/Modal/content/legacy/CounterSetting.tsx delete mode 100644 src/renderer/components/main/Modal/content/legacy/KeySetting.tsx diff --git a/src/renderer/components/main/Modal/ManagerModalLayout.tsx b/src/renderer/components/main/Modal/ManagerModalLayout.tsx index 68631b36..8d613341 100644 --- a/src/renderer/components/main/Modal/ManagerModalLayout.tsx +++ b/src/renderer/components/main/Modal/ManagerModalLayout.tsx @@ -22,6 +22,8 @@ interface ManagerModalLayoutProps { extra?: ReactNode; /** 콘텐츠 변경 시 높이/스크롤 재계산 트리거 의존성 */ contentDeps?: unknown[]; + /** 스크린리더용 다이얼로그 이름 */ + ariaLabel?: string; } const ManagerModalLayout = ({ @@ -32,6 +34,7 @@ const ManagerModalLayout = ({ footer, extra, contentDeps = [], + ariaLabel, }: ManagerModalLayoutProps) => { const contentRef = useRef(null); const [containerHeight, setContainerHeight] = useState(null); @@ -76,18 +79,18 @@ const ManagerModalLayout = ({ if (!isOpen) return null; return ( - +
e.stopPropagation()} > {/* 탭 영역 */} - {tabs &&
{tabs}
} + {tabs &&
{tabs}
} {/* 스크롤 영역 */}
-
+
{children}
- {/* 구분선 */} -
- {/* 하단 버튼 */} -
{footer}
+
{footer}
{/* 추가 콘텐츠 (로딩/에러 등) */} {extra} diff --git a/src/renderer/components/main/Modal/Modal.tsx b/src/renderer/components/main/Modal/Modal.tsx index ec752f52..4cceb415 100644 --- a/src/renderer/components/main/Modal/Modal.tsx +++ b/src/renderer/components/main/Modal/Modal.tsx @@ -5,12 +5,24 @@ interface ModalProps { onClick?: () => void; children: React.ReactNode; animate?: boolean; + /** 스크린리더용 다이얼로그 이름 */ + ariaLabel?: string; } -const Modal = ({ onClick, children, animate = true }: ModalProps) => { +const Modal = ({ + onClick, + children, + animate = true, + ariaLabel, +}: ModalProps) => { const scrimAnimClass = animate ? 'animate-modal-scrim' : ''; const contentAnimClass = animate ? 'animate-modal-scale' : ''; const closeFromBackdropRef = useRef(false); + const backdropRef = useRef(null); + const onCloseRef = useRef(onClick); + useEffect(() => { + onCloseRef.current = onClick; + }); useEffect(() => { const reset = () => { @@ -24,6 +36,39 @@ const Modal = ({ onClick, children, animate = true }: ModalProps) => { }; }, []); + // 닫힐 때 열기 전 포커스 복원 (요소가 아직 문서에 연결된 경우만) + useEffect(() => { + const prevFocused = document.activeElement as HTMLElement | null; + return () => { + if (prevFocused && prevFocused.isConnected) { + prevFocused.focus(); + } + }; + }, []); + + // Escape 닫기 — 레이어 소유권: 위 겹(팝업·서브메뉴·플로팅 피커)이 있으면 양보하고 + // 최상위 모달(마지막 마운트 백드롭)만 소비. 한 번에 한 겹씩 닫힘 + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if (e.key !== 'Escape' || e.defaultPrevented) return; + // 키 리스닝 중엔 양보 — Escape는 리스닝 취소로 예약됨 (raw input 레이스 포함) + if (window.__dmn_isKeyListening) return; + // 서브메뉴·플러그인 드롭다운이 위에 떠 있으면 그쪽이 소유 + if (document.querySelector('[data-dmn-popup-submenu="true"]')) return; + // FloatingPopup 계열(피커·컨텍스트 메뉴)이 위에 떠 있으면 그쪽이 소유 + if (document.querySelector('[role="dialog"][aria-modal="false"]')) return; + // 모달 스택에서 최상위만 소비 + const backdrops = document.querySelectorAll( + '[data-dmn-modal-backdrop="true"]', + ); + if (backdrops[backdrops.length - 1] !== backdropRef.current) return; + e.preventDefault(); + onCloseRef.current?.(); + }; + document.addEventListener('keydown', onKey); + return () => document.removeEventListener('keydown', onKey); + }, []); + const handleBackdropPointerDown = (e: React.PointerEvent) => { // only mark if pointer started directly on the backdrop closeFromBackdropRef.current = e.target === e.currentTarget; @@ -51,7 +96,11 @@ const Modal = ({ onClick, children, animate = true }: ModalProps) => { // 등장 모션은 스크림(틴트+블러 키프레임)과 콘텐츠 루트가 각자 소유한다 return createPortal(
{ > {/* 스크림 언더레이 — 클릭은 래퍼로 통과 */}