From 12e65aeff89e932f835f6e48e13407362899d6dc Mon Sep 17 00:00:00 2001 From: pa Date: Tue, 6 Jan 2026 22:37:46 +0900 Subject: [PATCH] replace el-splitter with resizable components --- src/components/NavMenu.vue | 8 +- .../ui/resizable/ResizableHandle.vue | 42 ++++++ .../ui/resizable/ResizablePanel.vue | 25 ++++ .../ui/resizable/ResizablePanelGroup.vue | 31 ++++ src/components/ui/resizable/index.js | 3 + .../useAuthenticatedLayoutResizable.js | 134 ++++++++++++++++++ src/stores/settings/appearance.js | 31 +++- src/views/Layout/AuthenticatedLayout.vue | 84 ++++++----- 8 files changed, 319 insertions(+), 39 deletions(-) create mode 100644 src/components/ui/resizable/ResizableHandle.vue create mode 100644 src/components/ui/resizable/ResizablePanel.vue create mode 100644 src/components/ui/resizable/ResizablePanelGroup.vue create mode 100644 src/components/ui/resizable/index.js create mode 100644 src/composables/useAuthenticatedLayoutResizable.js diff --git a/src/components/NavMenu.vue b/src/components/NavMenu.vue index 45e44642..65bbc594 100644 --- a/src/components/NavMenu.vue +++ b/src/components/NavMenu.vue @@ -798,10 +798,11 @@ .nav-menu-container { position: relative; - width: 240px; + width: 100%; + min-width: 64px; height: 100%; display: flex; - flex: 0 0 240px; + flex: 1 1 auto; flex-direction: column; align-items: stretch; justify-content: flex-start; @@ -914,8 +915,7 @@ } .nav-menu-container.is-collapsed { - width: 64px; - flex-basis: 64px; + width: 100%; } .nav-menu-container.is-collapsed .nav-menu :deep(.el-sub-menu__title > div) { diff --git a/src/components/ui/resizable/ResizableHandle.vue b/src/components/ui/resizable/ResizableHandle.vue new file mode 100644 index 00000000..8500318a --- /dev/null +++ b/src/components/ui/resizable/ResizableHandle.vue @@ -0,0 +1,42 @@ + + + diff --git a/src/components/ui/resizable/ResizablePanel.vue b/src/components/ui/resizable/ResizablePanel.vue new file mode 100644 index 00000000..440bc2f0 --- /dev/null +++ b/src/components/ui/resizable/ResizablePanel.vue @@ -0,0 +1,25 @@ + + + diff --git a/src/components/ui/resizable/ResizablePanelGroup.vue b/src/components/ui/resizable/ResizablePanelGroup.vue new file mode 100644 index 00000000..6f018633 --- /dev/null +++ b/src/components/ui/resizable/ResizablePanelGroup.vue @@ -0,0 +1,31 @@ + + + diff --git a/src/components/ui/resizable/index.js b/src/components/ui/resizable/index.js new file mode 100644 index 00000000..cffefef9 --- /dev/null +++ b/src/components/ui/resizable/index.js @@ -0,0 +1,3 @@ +export { default as ResizableHandle } from './ResizableHandle.vue'; +export { default as ResizablePanel } from './ResizablePanel.vue'; +export { default as ResizablePanelGroup } from './ResizablePanelGroup.vue'; diff --git a/src/composables/useAuthenticatedLayoutResizable.js b/src/composables/useAuthenticatedLayoutResizable.js new file mode 100644 index 00000000..ce880745 --- /dev/null +++ b/src/composables/useAuthenticatedLayoutResizable.js @@ -0,0 +1,134 @@ +import { computed, nextTick, onMounted, ref, watch } from 'vue'; +import { storeToRefs } from 'pinia'; + +import { useAppearanceSettingsStore } from '../stores'; + +export function useAuthenticatedLayoutResizable() { + const navCollapsedPx = 64; + const navDefaultPx = 240; + const navMinPx = 180; + const navMaxPx = 300; + const asideMaxPx = 500; + + const appearanceStore = useAppearanceSettingsStore(); + const { setAsideWidth, setNavWidth } = appearanceStore; + const { asideWidth, isNavCollapsed, isSideBarTabShow, navWidth } = + storeToRefs(appearanceStore); + + const panelGroupRef = ref(null); + const navPanelRef = ref(null); + const navExpandedSize = ref(null); + + const fallbackWidth = + typeof window !== 'undefined' && window.innerWidth + ? window.innerWidth + : 1200; + + const getGroupWidth = () => { + const element = panelGroupRef.value?.$el ?? panelGroupRef.value; + const width = element?.getBoundingClientRect?.().width; + return Number.isFinite(width) && width > 0 ? width : fallbackWidth; + }; + + const pxToPercent = (px, groupWidth, min = 1) => { + const w = groupWidth ?? getGroupWidth(); + return Math.min(100, Math.max(min, (px / w) * 100)); + }; + + const percentToPx = (percent, groupWidth) => (percent / 100) * groupWidth; + + const isAsideCollapsed = (layout) => + Array.isArray(layout) && + layout.length >= 3 && + layout[layout.length - 1] <= 1; + + const navCollapsedSize = computed(() => pxToPercent(navCollapsedPx)); + const navExpandedPx = computed(() => navWidth.value || navDefaultPx); + + const navDefaultSize = computed(() => + isNavCollapsed.value + ? navCollapsedSize.value + : pxToPercent(navExpandedPx.value) + ); + + const navMinSize = computed(() => + isNavCollapsed.value ? navCollapsedSize.value : pxToPercent(navMinPx) + ); + const navMaxSize = computed(() => + isNavCollapsed.value ? navCollapsedSize.value : pxToPercent(navMaxPx) + ); + + const asideDefaultSize = computed(() => + pxToPercent(asideWidth.value, undefined, 0) + ); + const asideMaxSize = computed(() => pxToPercent(asideMaxPx, undefined, 0)); + + const handleLayout = (sizes) => { + if (!Array.isArray(sizes) || sizes.length < 2) { + return; + } + + const groupWidth = getGroupWidth(); + if (!Number.isFinite(groupWidth) || groupWidth <= 0) { + return; + } + + const navSize = sizes[0]; + if (!isNavCollapsed.value && Number.isFinite(navSize) && navSize > 0) { + navExpandedSize.value = navSize; + setNavWidth(Math.round(percentToPx(navSize, groupWidth))); + } + + if (!isSideBarTabShow.value || sizes.length < 3) { + return; + } + + const asideSize = sizes[sizes.length - 1]; + if (!Number.isFinite(asideSize)) { + return; + } + + if (asideSize <= 1) { + setAsideWidth(0); + return; + } + + setAsideWidth(Math.round(percentToPx(asideSize, groupWidth))); + }; + + const resizeNavPanel = (targetSize) => + navPanelRef.value?.resize?.(targetSize); + + watch(isNavCollapsed, async (collapsed) => { + await nextTick(); + if (collapsed) { + resizeNavPanel(navCollapsedSize.value); + return; + } + const targetSize = + navExpandedSize.value ?? pxToPercent(navExpandedPx.value); + resizeNavPanel(targetSize); + }); + + onMounted(() => { + navExpandedSize.value = + navPanelRef.value?.getSize?.() ?? navDefaultSize.value; + if (isNavCollapsed.value) { + resizeNavPanel(navCollapsedSize.value); + } + }); + + return { + panelGroupRef, + navPanelRef, + navDefaultSize, + navMinSize, + navMaxSize, + asideDefaultSize, + asideMaxSize, + handleLayout, + isAsideCollapsed, + isNavCollapsed, + isSideBarTabShow + }; +} diff --git a/src/stores/settings/appearance.js b/src/stores/settings/appearance.js index 066b12ec..af31bbe7 100644 --- a/src/stores/settings/appearance.js +++ b/src/stores/settings/appearance.js @@ -68,6 +68,7 @@ export const useAppearanceSettingsStore = defineStore( 'Sort by Last Active' ]); const asideWidth = ref(300); + const navWidth = ref(240); const isSidebarGroupByInstance = ref(true); const isHideFriendsInSameInstance = ref(false); const isSidebarDivideByFriendGroup = ref(false); @@ -119,6 +120,7 @@ export const useAppearanceSettingsStore = defineStore( dtIsoFormatConfig, sidebarSortMethodsConfig, asideWidthConfig, + navWidthConfig, isSidebarGroupByInstanceConfig, isHideFriendsInSameInstanceConfig, isSidebarDivideByFriendGroupConfig, @@ -164,6 +166,7 @@ export const useAppearanceSettingsStore = defineStore( ]) ), configRepository.getInt('VRCX_sidePanelWidth', 300), + configRepository.getInt('VRCX_navPanelWidth', 240), configRepository.getBool('VRCX_sidebarGroupByInstance', true), configRepository.getBool( 'VRCX_hideFriendsInSameInstance', @@ -248,6 +251,7 @@ export const useAppearanceSettingsStore = defineStore( } trustColor.value = { ...TRUST_COLOR_DEFAULTS }; asideWidth.value = asideWidthConfig; + navWidth.value = clampInt(navWidthConfig, 64, 480); isSidebarGroupByInstance.value = isSidebarGroupByInstanceConfig; isHideFriendsInSameInstance.value = isHideFriendsInSameInstanceConfig; @@ -620,7 +624,7 @@ export const useAppearanceSettingsStore = defineStore( function toggleNavCollapsed() { setNavCollapsed(!isNavCollapsed.value); } - function setAsideWidth(widthOrArray) { + function setNavWidth(widthOrArray) { let width = null; if (Array.isArray(widthOrArray) && widthOrArray.length) { width = widthOrArray[widthOrArray.length - 1]; @@ -629,11 +633,30 @@ export const useAppearanceSettingsStore = defineStore( } if (width) { requestAnimationFrame(() => { - asideWidth.value = width; - configRepository.setInt('VRCX_sidePanelWidth', width); + navWidth.value = clampInt(width, 64, 480); + configRepository.setInt( + 'VRCX_navPanelWidth', + navWidth.value + ); }); } } + function setAsideWidth(widthOrArray) { + let width = null; + if (Array.isArray(widthOrArray) && widthOrArray.length) { + width = widthOrArray[widthOrArray.length - 1]; + } else if (typeof widthOrArray === 'number') { + width = widthOrArray; + } + if (!Number.isFinite(width) || width === null) { + return; + } + const normalized = Math.max(0, Math.round(width)); + requestAnimationFrame(() => { + asideWidth.value = normalized; + configRepository.setInt('VRCX_sidePanelWidth', normalized); + }); + } function setIsSidebarGroupByInstance() { isSidebarGroupByInstance.value = !isSidebarGroupByInstance.value; configRepository.setBool( @@ -840,6 +863,7 @@ export const useAppearanceSettingsStore = defineStore( sidebarSortMethod3, sidebarSortMethods, asideWidth, + navWidth, isSidebarGroupByInstance, isHideFriendsInSameInstance, isSidebarDivideByFriendGroup, @@ -869,6 +893,7 @@ export const useAppearanceSettingsStore = defineStore( setSidebarSortMethod2, setSidebarSortMethod3, setSidebarSortMethods, + setNavWidth, setAsideWidth, setIsSidebarGroupByInstance, setIsHideFriendsInSameInstance, diff --git a/src/views/Layout/AuthenticatedLayout.vue b/src/views/Layout/AuthenticatedLayout.vue index 73b59d37..fa83755c 100644 --- a/src/views/Layout/AuthenticatedLayout.vue +++ b/src/views/Layout/AuthenticatedLayout.vue @@ -1,19 +1,43 @@