From efb87b6aa7d338cd02bebe3634c0443d2312f374 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=97=B0=EC=9A=B0?= Date: Sun, 8 Nov 2026 01:39:24 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=EC=98=A4=EB=B2=84=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=20=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EB=A7=89=EB=8C=80=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../overlayScrollbar/OverlayScrollbar.tsx | 67 +++ .../overlayScrollbarController.test.ts | 356 ++++++++++++ .../overlayScrollbarController.ts | 519 ++++++++++++++++++ .../overlayScrollbarModel.test.ts | 241 ++++++++ .../overlayScrollbar/overlayScrollbarModel.ts | 131 +++++ src/renderer/hooks/useLenis.ts | 54 ++ 6 files changed, 1368 insertions(+) create mode 100644 src/renderer/components/main/common/overlayScrollbar/OverlayScrollbar.tsx create mode 100644 src/renderer/components/main/common/overlayScrollbar/overlayScrollbarController.test.ts create mode 100644 src/renderer/components/main/common/overlayScrollbar/overlayScrollbarController.ts create mode 100644 src/renderer/components/main/common/overlayScrollbar/overlayScrollbarModel.test.ts create mode 100644 src/renderer/components/main/common/overlayScrollbar/overlayScrollbarModel.ts diff --git a/src/renderer/components/main/common/overlayScrollbar/OverlayScrollbar.tsx b/src/renderer/components/main/common/overlayScrollbar/OverlayScrollbar.tsx new file mode 100644 index 000000000..7356fd733 --- /dev/null +++ b/src/renderer/components/main/common/overlayScrollbar/OverlayScrollbar.tsx @@ -0,0 +1,67 @@ +import { useEffect, useRef } from 'react'; + +import { usePanelHost } from '@contexts/PanelHostContext'; + +import { + createOverlayScrollbarController, + type OverlayScrollbarController, + type OverlayScrollbarPlacement, +} from './overlayScrollbarController'; + +interface OverlayScrollbarProps { + /** 끌어 정렬처럼 뷰포트 위 히트 판정이 필요한 동안 막대 입력을 끈다 */ + inert?: boolean; + /** 레일 왼쪽 위치(뷰포트 기준 px). edge 배치에서만 쓴다 */ + railLeft?: number; + /** edge는 뷰포트 오른쪽 끝 안쪽, outside는 부모 상자 바깥 오른쪽 여백 */ + placement?: OverlayScrollbarPlacement; + className?: string; +} + +/** + * 오버레이 스크롤 막대. 스크롤 뷰포트와 같은 부모(위치 지정 요소) 안 마지막 자식으로 둔다. + * 네이티브 막대는 전역에서 숨긴 채, 스크롤할 때와 가장자리에 포인터가 닿을 때만 드러난다. + * 레일 자리(오른쪽 띠)에는 버튼이 없어야 한다 - 쓰는 쪽 레이아웃이 그 띠를 비워 둔다 + */ +const OverlayScrollbar = ({ + inert = false, + railLeft, + placement = 'edge', + className, +}: OverlayScrollbarProps) => { + // 분리 패널로 옮겨지면 그 창 기준으로 관찰자를 다시 만든다 + const { window: ownerWindow } = usePanelHost(); + const railRef = useRef(null); + const thumbRef = useRef(null); + const controllerRef = useRef(null); + + useEffect(() => { + const rail = railRef.current; + const thumb = thumbRef.current; + if (!rail || !thumb) return undefined; + const view = (rail.ownerDocument.defaultView ?? ownerWindow) as Window & + typeof globalThis; + const controller = createOverlayScrollbarController(rail, thumb, view); + controllerRef.current = controller; + return () => { + controller.destroy(); + controllerRef.current = null; + }; + }, [ownerWindow]); + + useEffect(() => { + controllerRef.current?.setOptions({ inert, railLeft, placement }); + }, [inert, railLeft, placement, ownerWindow]); + + return ( + + ); diff --git a/src/renderer/components/main/Grid/PropertiesPanel/document/DocumentBatchPanel.tsx b/src/renderer/components/main/Grid/PropertiesPanel/document/DocumentBatchPanel.tsx index ecbab23f7..dceda7a3d 100644 --- a/src/renderer/components/main/Grid/PropertiesPanel/document/DocumentBatchPanel.tsx +++ b/src/renderer/components/main/Grid/PropertiesPanel/document/DocumentBatchPanel.tsx @@ -52,6 +52,7 @@ import { LegacyKeyLikeBatchBody, type LegacyKeyLikeHost, } from './legacyKeyLikeBatch'; +import OverlayScrollbar from '@components/main/common/overlayScrollbar/OverlayScrollbar'; // 옛 종류 선택의 일괄 기하 - 속성 패널이 옛 선택 모양(플러그인 요소 포함)에서 만든다 export type LegacyBatchGeometry = BatchGeometry; @@ -367,6 +368,7 @@ const DocumentBatchPanel = ({ {legacyHost?.pluginSettings} + ); diff --git a/src/renderer/components/main/Grid/PropertiesPanel/document/DocumentElementPanel.tsx b/src/renderer/components/main/Grid/PropertiesPanel/document/DocumentElementPanel.tsx index 157993709..542d1605c 100644 --- a/src/renderer/components/main/Grid/PropertiesPanel/document/DocumentElementPanel.tsx +++ b/src/renderer/components/main/Grid/PropertiesPanel/document/DocumentElementPanel.tsx @@ -45,6 +45,7 @@ import { singlePanelWriterFor } from './documentPanelWriter'; import { LegacyCustomCssSection, LegacySingleStyle } from './legacyBundleRows'; import { KeyStatGeometry } from './legacyKeyStatSections'; import type { PanelTab } from '@src/renderer/elementKinds/defineElementKind'; +import OverlayScrollbar from '@components/main/common/overlayScrollbar/OverlayScrollbar'; interface DocumentElementPanelProps { sceneId: string; @@ -380,6 +381,7 @@ const ElementPanelBody = ({ {tabBody('style')} )} + ); diff --git a/src/renderer/components/main/Grid/PropertiesPanel/document/legacyKeyLikeBatch.tsx b/src/renderer/components/main/Grid/PropertiesPanel/document/legacyKeyLikeBatch.tsx index 2c1a7c9e4..8c0743944 100644 --- a/src/renderer/components/main/Grid/PropertiesPanel/document/legacyKeyLikeBatch.tsx +++ b/src/renderer/components/main/Grid/PropertiesPanel/document/legacyKeyLikeBatch.tsx @@ -70,6 +70,7 @@ import { } from './documentPanelModel'; import { PanelSections, type PanelSectionRenderers } from './documentPanelRows'; import { canEditBundleField, type UnionSection } from './documentPanelUnion'; +import OverlayScrollbar from '@components/main/common/overlayScrollbar/OverlayScrollbar'; // 속성 패널이 갖는 일괄 피커 상태 - 선택이 바뀌면 속성 패널이 닫는다 export interface LegacyKeyLikeHost { @@ -635,6 +636,7 @@ export const LegacyKeyLikeBatchBody = ({ {tabBody('style')} )} + void; } +// 오른쪽 12px는 스크롤 막대 자리 - 숨김 토글 버튼이 그 띠에 걸리지 않게 비워 둔다 const rowClass = ( selected: boolean, hidden: boolean, dragging: boolean, dimmed = false, ) => ` - group relative flex items-center gap-[8px] pr-[4px] h-[34px] select-none dmn-row-grabbable + group relative flex items-center gap-[8px] pr-[12px] h-[34px] select-none dmn-row-grabbable ${dimmed ? 'opacity-30' : hidden ? 'opacity-60' : ''} ${ selected @@ -200,7 +201,7 @@ export const LayerGroupRow = ({ style={{ paddingLeft: layerRowIndent(depth) }} className={ dropInto - ? 'group relative flex items-center gap-[8px] pr-[4px] h-[34px] select-none bg-fill-hover text-fg-muted' + ? 'group relative flex items-center gap-[8px] pr-[12px] h-[34px] select-none bg-fill-hover text-fg-muted' : rowClass(selected, hidden, dragging, dimmed) } > diff --git a/src/renderer/components/main/Grid/PropertiesPanel/layer/LayerTabContent.tsx b/src/renderer/components/main/Grid/PropertiesPanel/layer/LayerTabContent.tsx index d159771b7..543d68bdb 100644 --- a/src/renderer/components/main/Grid/PropertiesPanel/layer/LayerTabContent.tsx +++ b/src/renderer/components/main/Grid/PropertiesPanel/layer/LayerTabContent.tsx @@ -54,6 +54,7 @@ import { type LayerRowRename, } from './LayerRows'; import { useLayerCollapseStore } from '@stores/grid/useLayerCollapseStore'; +import OverlayScrollbar from '@components/main/common/overlayScrollbar/OverlayScrollbar'; // 끌기로 보지 않는 움직임 (화면 px) // 예전 레이어 끌기와 같은 시작 거리 (가로·세로 어느 쪽이든) @@ -771,6 +772,7 @@ const LayerTabContent = ({ )} + {contextMenuPresence.mounted && createPortal( diff --git a/src/renderer/components/main/Grid/PropertiesPanel/plugin/PluginSettingsPanelView.tsx b/src/renderer/components/main/Grid/PropertiesPanel/plugin/PluginSettingsPanelView.tsx index 359c00772..ba0945dd8 100644 --- a/src/renderer/components/main/Grid/PropertiesPanel/plugin/PluginSettingsPanelView.tsx +++ b/src/renderer/components/main/Grid/PropertiesPanel/plugin/PluginSettingsPanelView.tsx @@ -14,6 +14,7 @@ import { hasRenderableSettings, type SettingsNormalizationErrorKind, } from '@plugins/runtime/settingsSections'; +import OverlayScrollbar from '@components/main/common/overlayScrollbar/OverlayScrollbar'; interface PluginSettingsPanelViewProps { setPanelElement: (el: HTMLDivElement | null) => void; @@ -100,6 +101,7 @@ const PluginSettingsPanelView: 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 3b89e507e..8b68ddf2d 100644 --- a/src/renderer/components/main/Grid/PropertiesPanel/single/SingleSelectionPanel.tsx +++ b/src/renderer/components/main/Grid/PropertiesPanel/single/SingleSelectionPanel.tsx @@ -22,6 +22,7 @@ import { } from '../controls/PropertyInputs'; import { AXIS_FIELD_WIDTH } from '@utils/cardRecipes'; import EditSessionBoundary from '../selection/EditSessionBoundary'; +import OverlayScrollbar from '@components/main/common/overlayScrollbar/OverlayScrollbar'; // ============================================================================ // Plugin Selection Panel @@ -256,6 +257,7 @@ export const PluginSelectionPanel: React.FC = ({ )}
+ ); diff --git a/src/renderer/components/main/Modal/content/pickers/CommonListPickerPage.tsx b/src/renderer/components/main/Modal/content/pickers/CommonListPickerPage.tsx index 28372a763..58c1fc8f5 100644 --- a/src/renderer/components/main/Modal/content/pickers/CommonListPickerPage.tsx +++ b/src/renderer/components/main/Modal/content/pickers/CommonListPickerPage.tsx @@ -3,6 +3,7 @@ import { useLenis } from '@hooks/useLenis'; import Dropdown from '@components/main/common/dropdown/Dropdown'; import SearchField from '@components/main/common/SearchField'; import ListAddRow from '@components/main/common/ListAddRow'; +import OverlayScrollbar from '@components/main/common/overlayScrollbar/OverlayScrollbar'; type FilterOption = { value: string; @@ -167,7 +168,7 @@ export default function CommonListPickerPage({ /> {/* 리스트 컨테이너 — 배경보다 한 단계 밝은 필 테이블. 빈 공간도 테이블의 빈 영역으로 읽힘 */} -
+
({ )}
+ {/* 행과 ⋮ 버튼이 상자 끝까지 차 있어 막대는 상자 바깥 12px 여백에 둔다 */} +
{/* 하단 필터 바 */}
diff --git a/src/renderer/components/main/Settings.tsx b/src/renderer/components/main/Settings.tsx index 28260b639..e55adc31b 100644 --- a/src/renderer/components/main/Settings.tsx +++ b/src/renderer/components/main/Settings.tsx @@ -49,6 +49,7 @@ import { cssApi } from '@api/modules/resources/cssApi'; import { jsApi } from '@api/modules/plugin/jsApi'; import { keysApi } from '@api/modules/editor/keysApi'; import { appApi, windowApi } from '@api/modules/app/appApi'; +import OverlayScrollbar from '@components/main/common/overlayScrollbar/OverlayScrollbar'; interface SettingsProps { showAlert: (msg: string, confirmText?: string) => void; @@ -64,6 +65,10 @@ interface SettingsProps { ) => void; } +// 스크롤 컨테이너가 창 전체 폭이라 막대를 오른쪽 끝에 두면 프리뷰 옆에 붙는다. +// 카드 열(패딩 12 + 폭 348)과 프리뷰 페인(left 372) 사이 12px 골에 둔다 +const SETTINGS_RAIL_LEFT = 360; + const Settings = ({ showAlert, showConfirm, @@ -739,6 +744,10 @@ const Settings = ({
+ {/* 우측 고정 콘텐츠 페인 - 기본은 튜토리얼 프리뷰, 선택 시 설정 상세가 같은 표면을 채움 */}
= new Set([ 'PATH_NOT_AUTHORIZED', @@ -334,6 +335,7 @@ const CssPanelContent = ({
)} + {/* 하단 바 - 불러오기(주 액션, 크게) + 닫기 */} diff --git a/src/renderer/components/main/SettingsPanel/PluginsPanelContent.tsx b/src/renderer/components/main/SettingsPanel/PluginsPanelContent.tsx index 56769f665..45634c08e 100644 --- a/src/renderer/components/main/SettingsPanel/PluginsPanelContent.tsx +++ b/src/renderer/components/main/SettingsPanel/PluginsPanelContent.tsx @@ -25,6 +25,7 @@ import { usePickerItemMenu } from '@hooks/usePickerItemMenu'; import { usePluginHealthStore } from '@stores/plugin/usePluginHealthStore'; import { getPluginDisplayName } from '@utils/plugin/components/pluginUtils'; import type { JsPlugin } from '@src/types/plugin/js'; +import OverlayScrollbar from '@components/main/common/overlayScrollbar/OverlayScrollbar'; interface PluginsPanelContentProps { plugins: JsPlugin[]; @@ -146,6 +147,7 @@ const PluginsPanelContent = ({ )} + {/* 하단 바 - 추가(주 액션, 크게) + 닫기 */} diff --git a/src/renderer/components/main/SettingsPanel/ShortcutsPanelContent.tsx b/src/renderer/components/main/SettingsPanel/ShortcutsPanelContent.tsx index 20fccf42e..65ab1688a 100644 --- a/src/renderer/components/main/SettingsPanel/ShortcutsPanelContent.tsx +++ b/src/renderer/components/main/SettingsPanel/ShortcutsPanelContent.tsx @@ -14,6 +14,7 @@ import type { } from '@src/types/settings/shortcuts'; import { getDefaultShortcuts } from '@src/renderer/defaults'; import { isHistoryEditorFlushLocked } from '@src/renderer/editor/runtime/lifecycle/historyEditorFlushLock'; +import OverlayScrollbar from '@components/main/common/overlayScrollbar/OverlayScrollbar'; type ShortcutKey = keyof ShortcutsState; @@ -321,7 +322,7 @@ const ShortcutsPanelContent = ({ return (
{ if (!isListening) return; const target = event.target as HTMLElement | null; @@ -353,6 +354,7 @@ const ShortcutsPanelContent = ({ ) : null}
+ {/* 하단 바 - 닫기 */}
diff --git a/src/renderer/components/main/SettingsPanel/panelChrome.ts b/src/renderer/components/main/SettingsPanel/panelChrome.ts index 10cfa0a01..950f06317 100644 --- a/src/renderer/components/main/SettingsPanel/panelChrome.ts +++ b/src/renderer/components/main/SettingsPanel/panelChrome.ts @@ -39,7 +39,7 @@ export const PANEL_PILL_CLASS = // 인셋 리스트 웰 + 내부 스크롤 + 빈 상태 + 행 이름 export const PANEL_LIST_WELL_CLASS = - 'mx-[12px] mb-[12px] bg-inset rounded-surface flex-1 min-h-0 flex flex-col'; + 'relative mx-[12px] mb-[12px] bg-inset rounded-surface flex-1 min-h-0 flex flex-col'; // 마스터 스위치가 꺼져도 목록은 가라앉히지 않는다. 그룹 opacity는 켜짐 알약의 // 면과 항목 이름까지 같이 깎아, 정작 읽어야 할 '무엇이 켜져 있나'를 지운다. // 마스터 상태는 바로 위 토글이 이미 말하고 있다 From 26335d1841b50274ef870fadb173b9c9e82cabf0 Mon Sep 17 00:00:00 2001 From: lee-sihun Date: Sun, 8 Nov 2026 11:11:13 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20=EC=8A=A4=ED=81=AC=EB=A1=A4=20?= =?UTF-8?q?=EB=A7=89=EB=8C=80=EA=B0=80=20Lenis=20=EB=AA=A9=ED=91=9C=20?= =?UTF-8?q?=EC=A7=80=EC=A0=90=EC=9D=84=20=EB=8D=AE=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EA=B3=A0=20=EA=B4=80=EC=B0=B0=EC=9E=90=EA=B0=80=20=EC=9E=A1?= =?UTF-8?q?=EC=9D=80=20=ED=81=AC=EA=B8=B0=20=EB=B3=80=ED=99=94=EB=8F=84=20?= =?UTF-8?q?=ED=95=9C=EB=8F=84=EC=97=90=20=EB=B0=98=EC=98=81=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../overlayScrollbarController.test.ts | 54 +++++++++++++++++-- .../overlayScrollbarController.ts | 20 ++++--- 2 files changed, 58 insertions(+), 16 deletions(-) diff --git a/src/renderer/components/main/common/overlayScrollbar/overlayScrollbarController.test.ts b/src/renderer/components/main/common/overlayScrollbar/overlayScrollbarController.test.ts index f0714ff87..4a700be16 100644 --- a/src/renderer/components/main/common/overlayScrollbar/overlayScrollbarController.test.ts +++ b/src/renderer/components/main/common/overlayScrollbar/overlayScrollbarController.test.ts @@ -206,11 +206,10 @@ describe('createOverlayScrollbarController', () => { controller.destroy(); }); - it('깊은 자손만 커졌으면 휠을 Lenis보다 먼저 받아 한도를 다시 재게 한다', () => { - const viewport = addViewport({ clientHeight: 400, scrollHeight: 400 }); - // 실제 Lenis처럼 resize()가 구독자에게 'scroll'을 동기로 낸다 + // 실제 Lenis처럼 resize()는 목표 지점을 현재 위치로 덮고 'scroll'을 동기로 낸다. + // 한도만 다시 재는 dimensions.resize()는 이벤트를 내지 않는다 + const mockLenis = (order: string[] = []) => { let onLenisScroll: (() => void) | undefined; - const order: string[] = []; const lenis = { on: vi.fn((_event: string, callback: () => void) => { onLenisScroll = callback; @@ -220,23 +219,68 @@ describe('createOverlayScrollbarController', () => { order.push('resize'); onLenisScroll?.(); }), + dimensions: { + resize: vi.fn(() => { + order.push('dimensions'); + }), + }, + emitScroll: () => onLenisScroll?.(), }; lenisMocks.getWrapperLenis.mockReturnValue(lenis as never); + return lenis; + }; + + it('깊은 자손만 커졌으면 휠을 Lenis보다 먼저 받아 한도를 다시 재게 한다', () => { + const viewport = addViewport({ clientHeight: 400, scrollHeight: 400 }); + const order: string[] = []; + const lenis = mockLenis(order); const controller = create(); expect(lenis.on).toHaveBeenCalledWith('scroll', expect.any(Function)); viewport.addEventListener('wheel', () => order.push('lenis-wheel')); + order.length = 0; setBox(viewport, { scrollHeight: 800 }); viewport.dispatchEvent( new WheelEvent('wheel', { deltaY: 40, bubbles: true }), ); - expect(order).toEqual(['resize', 'lenis-wheel']); + expect(order).toEqual(['dimensions', 'lenis-wheel']); // 크기만 다시 잰다. 드러내는 건 이어지는 실제 스크롤이 맡는다 expect(rail.dataset.state).toBe('hidden'); expect(thumb.style.height).toBe('196px'); controller.destroy(); }); + it('부드러운 스크롤 중에 높이가 바뀌어도 Lenis 목표 지점을 덮지 않는다', () => { + const viewport = addViewport({ clientHeight: 400, scrollHeight: 1600 }); + const lenis = mockLenis(); + const controller = create(); + setBox(viewport, { scrollHeight: 2000 }); + lenis.emitScroll(); + expect(lenis.resize).not.toHaveBeenCalled(); + expect(lenis.dimensions.resize).toHaveBeenCalled(); + controller.destroy(); + }); + + it('관찰자가 잡은 크기 변화도 Lenis 한도에 반영한다', () => { + const viewport = addViewport({ clientHeight: 400, scrollHeight: 400 }); + const lenis = mockLenis(); + const controller = create(); + lenis.dimensions.resize.mockClear(); + + setBox(viewport, { scrollHeight: 1600 }); + ResizeObserverStub.instances.forEach((observer) => observer.callback()); + expect(lenis.dimensions.resize).toHaveBeenCalledTimes(1); + expect(thumb.style.height).toBe('98px'); + + // 이미 반영된 크기라 다음 휠에서 다시 재지 않는다 + viewport.dispatchEvent( + new WheelEvent('wheel', { deltaY: 40, bubbles: true }), + ); + expect(lenis.dimensions.resize).toHaveBeenCalledTimes(1); + expect(lenis.resize).not.toHaveBeenCalled(); + controller.destroy(); + }); + it('직계 자식 박스는 그대로이고 깊은 자손만 커져도 스크롤 때 다시 잰다', () => { const viewport = addViewport({ clientHeight: 400, scrollHeight: 1600 }); const controller = create(); diff --git a/src/renderer/components/main/common/overlayScrollbar/overlayScrollbarController.ts b/src/renderer/components/main/common/overlayScrollbar/overlayScrollbarController.ts index 11996887d..de8165dad 100644 --- a/src/renderer/components/main/common/overlayScrollbar/overlayScrollbarController.ts +++ b/src/renderer/components/main/common/overlayScrollbar/overlayScrollbarController.ts @@ -98,7 +98,6 @@ export const createOverlayScrollbarController = ( let lenis: ReturnType; let unsubscribeLenis: (() => void) | null = null; let destroyed = false; - let syncingContentSize = false; const observed = new Set(); const applyState = () => { @@ -157,8 +156,14 @@ export const createOverlayScrollbarController = ( ); viewportVisible = true; + const sizeChanged = + viewportSize !== viewport.clientHeight || + contentSize !== viewport.scrollHeight; viewportSize = viewport.clientHeight; contentSize = viewport.scrollHeight; + // 크기가 바뀌면 Lenis 한도도 함께 다시 잰다 - Lenis 관찰자는 깊은 자손 변화를 놓칠 수 있다. + // resize()는 진행 중인 부드러운 스크롤 목표를 현재 위치로 덮으므로 dimensions만 다시 잰다 + if (sizeChanged) getWrapperLenis(viewport)?.dimensions.resize(); let top = 0; let left: number; @@ -196,22 +201,15 @@ export const createOverlayScrollbarController = ( // 바뀐 경우를 휠·스크롤·호버 시점에 바로잡는다. 같은 이유로 Lenis 한도도 낡았을 수 있어 // 함께 다시 잰다 - 안 그러면 휠이 이전 한도에 묶여 새 영역으로 못 간다. 바뀌었으면 true const syncContentSize = (): boolean => { - if (!viewport || syncingContentSize) return false; + if (!viewport) return false; if ( viewport.scrollHeight === contentSize && viewport.clientHeight === viewportSize ) { return false; } - syncingContentSize = true; - try { - // 캐시를 먼저 갱신한다 - Lenis resize()는 'scroll'을 동기로 내서 여기로 다시 들어온다 - measure(); - getWrapperLenis(viewport)?.resize(); - applyState(); - } finally { - syncingContentSize = false; - } + measure(); + applyState(); return true; };