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 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
@@ -55,11 +79,11 @@