mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-21 07:43:50 +02:00
sidebar virtual dom and textfield row sizing
This commit is contained in:
@@ -1,173 +1,92 @@
|
||||
<template>
|
||||
<div class="x-friend-list" style="padding: 10px 5px">
|
||||
<div
|
||||
class="x-friend-group cursor-pointer flex items-center"
|
||||
style="padding: 0 0 5px"
|
||||
@click="
|
||||
isFriendsGroupMe = !isFriendsGroupMe;
|
||||
saveFriendsGroupStates();
|
||||
">
|
||||
<ChevronDown class="rotation-transition" :class="{ 'is-rotated': !isFriendsGroupMe }" />
|
||||
<span style="margin-left: 5px">{{ t('side_panel.me') }}</span>
|
||||
</div>
|
||||
<div v-show="isFriendsGroupMe">
|
||||
<div class="x-friend-item" @click="showUserDialog(currentUser.id)">
|
||||
<div class="avatar" :class="userStatusClass(currentUser)">
|
||||
<img :src="userImage(currentUser)" loading="lazy" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span class="name" :style="{ color: currentUser.$userColour }">{{ currentUser.displayName }}</span>
|
||||
<Location
|
||||
v-if="isGameRunning && !gameLogDisabled"
|
||||
class="text-xs"
|
||||
:location="lastLocation.location"
|
||||
:traveling="lastLocationDestination"
|
||||
:link="false" />
|
||||
<Location
|
||||
v-else-if="
|
||||
isRealInstance(currentUser.$locationTag) || isRealInstance(currentUser.$travelingToLocation)
|
||||
"
|
||||
class="text-xs"
|
||||
:location="currentUser.$locationTag"
|
||||
:traveling="currentUser.$travelingToLocation"
|
||||
:link="false" />
|
||||
|
||||
<span v-else class="text-xs">{{ currentUser.statusDescription }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-show="vipFriendsDisplayNumber"
|
||||
class="x-friend-group cursor-pointer flex items-center"
|
||||
@click="
|
||||
isVIPFriends = !isVIPFriends;
|
||||
saveFriendsGroupStates();
|
||||
">
|
||||
<ChevronDown class="rotation-transition" :class="{ 'is-rotated': !isVIPFriends }" />
|
||||
<span style="margin-left: 5px">
|
||||
{{ t('side_panel.favorite') }} ―
|
||||
{{ vipFriendsDisplayNumber }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-show="isVIPFriends">
|
||||
<template v-if="isSidebarDivideByFriendGroup">
|
||||
<div v-for="group in vipFriendsDivideByGroup" :key="group[0].key">
|
||||
<transition name="el-fade-in-linear">
|
||||
<div v-show="group[0].groupName !== ''" style="margin-bottom: 3px">
|
||||
<span class="text-xs">{{ group[0].groupName }}</span>
|
||||
<span class="text-xs" style="margin-left: 5px">{{ `(${group.length})` }}</span>
|
||||
<div ref="listRootRef" class="x-friend-list" style="padding: 10px 5px">
|
||||
<div v-if="virtualRows.length" class="friend-sidebar__virtual" :style="virtualContainerStyle">
|
||||
<template v-for="item in virtualItems" :key="String(item.virtualItem.key)">
|
||||
<div
|
||||
v-if="item.row"
|
||||
class="friend-sidebar__virtual-row"
|
||||
:class="`friend-sidebar__virtual-row--${item.row.type}`"
|
||||
:data-index="item.virtualItem.index"
|
||||
:ref="virtualizer.measureElement"
|
||||
:style="rowStyle(item)">
|
||||
<template v-if="item.row.type === 'toggle-header'">
|
||||
<div
|
||||
class="x-friend-group cursor-pointer flex items-center"
|
||||
:style="item.row.headerPadding ? { padding: item.row.headerPadding } : undefined"
|
||||
@click="item.row.onClick && item.row.onClick()">
|
||||
<ChevronDown class="rotation-transition" :class="{ 'is-rotated': !item.row.expanded }" />
|
||||
<span style="margin-left: 5px">
|
||||
{{ item.row.label }}
|
||||
<template v-if="item.row.count !== null && item.row.count !== undefined">
|
||||
― {{ item.row.count }}
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
</transition>
|
||||
<div v-if="group.length" style="margin-bottom: 10px">
|
||||
</template>
|
||||
|
||||
<template v-else-if="item.row.type === 'me-item'">
|
||||
<div class="x-friend-item" @click="showUserDialog(currentUser.id)">
|
||||
<div class="avatar" :class="userStatusClass(currentUser)">
|
||||
<img :src="userImage(currentUser)" loading="lazy" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<span class="name" :style="{ color: currentUser.$userColour }">{{
|
||||
currentUser.displayName
|
||||
}}</span>
|
||||
<Location
|
||||
v-if="isGameRunning && !gameLogDisabled"
|
||||
class="text-xs"
|
||||
:location="lastLocation.location"
|
||||
:traveling="lastLocationDestination"
|
||||
:link="false" />
|
||||
<Location
|
||||
v-else-if="
|
||||
isRealInstance(currentUser.$locationTag) ||
|
||||
isRealInstance(currentUser.$travelingToLocation)
|
||||
"
|
||||
class="text-xs"
|
||||
:location="currentUser.$locationTag"
|
||||
:traveling="currentUser.$travelingToLocation"
|
||||
:link="false" />
|
||||
|
||||
<span v-else class="text-xs">{{ currentUser.statusDescription }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="item.row.type === 'vip-subheader'">
|
||||
<div>
|
||||
<span class="text-xs">{{ item.row.label }}</span>
|
||||
<span class="text-xs" style="margin-left: 5px">{{ `(${item.row.count})` }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="item.row.type === 'instance-header'">
|
||||
<div class="mb-1 flex items-center">
|
||||
<Location class="text-xs" :location="item.row.location" style="display: inline" />
|
||||
<span class="text-xs" style="margin-left: 5px">{{ `(${item.row.count})` }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="item.row.type === 'friend-item'">
|
||||
<friend-item
|
||||
v-for="friend in group"
|
||||
:key="friend.id"
|
||||
:friend="friend"
|
||||
@confirm-delete-friend="confirmDeleteFriend"></friend-item>
|
||||
</div>
|
||||
:friend="item.row.friend"
|
||||
:style="item.row.itemStyle"
|
||||
:is-group-by-instance="item.row.isGroupByInstance"
|
||||
@confirm-delete-friend="confirmDeleteFriend" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<friend-item
|
||||
v-for="friend in vipFriendsByGroupStatus"
|
||||
:key="friend.id"
|
||||
:friend="friend"
|
||||
@confirm-delete-friend="confirmDeleteFriend">
|
||||
</friend-item>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template v-if="isSidebarGroupByInstance && friendsInSameInstance.length">
|
||||
<div class="x-friend-group cursor-pointer flex items-center" @click="toggleSwitchGroupByInstanceCollapsed">
|
||||
<ChevronDown class="rotation-transition" :class="{ 'is-rotated': isSidebarGroupByInstanceCollapsed }" />
|
||||
<span style="margin-left: 5px"
|
||||
>{{ t('side_panel.same_instance') }} ― {{ friendsInSameInstance.length }}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div v-show="!isSidebarGroupByInstanceCollapsed">
|
||||
<div v-for="friendArr in friendsInSameInstance" :key="friendArr[0].ref.$location.tag">
|
||||
<div class="mb-1 flex items-center">
|
||||
<Location
|
||||
class="text-xs text-muted-foreground"
|
||||
:location="getFriendsLocations(friendArr)"
|
||||
style="display: inline" />
|
||||
<span class="text-xs" style="margin-left: 5px">{{ `(${friendArr.length})` }}</span>
|
||||
</div>
|
||||
<div v-if="friendArr && friendArr.length">
|
||||
<friend-item
|
||||
v-for="(friend, idx) in friendArr"
|
||||
:key="friend.id"
|
||||
:friend="friend"
|
||||
is-group-by-instance
|
||||
:style="{ 'margin-bottom': idx === friendArr.length - 1 ? '5px' : undefined }"
|
||||
@confirm-delete-friend="confirmDeleteFriend">
|
||||
</friend-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
v-show="onlineFriendsByGroupStatus.length"
|
||||
class="x-friend-group cursor-pointer flex items-center"
|
||||
@click="
|
||||
isOnlineFriends = !isOnlineFriends;
|
||||
saveFriendsGroupStates();
|
||||
">
|
||||
<ChevronDown class="rotation-transition" :class="{ 'is-rotated': !isOnlineFriends }" />
|
||||
<span style="margin-left: 5px"
|
||||
>{{ t('side_panel.online') }} ― {{ onlineFriendsByGroupStatus.length }}</span
|
||||
>
|
||||
</div>
|
||||
<div v-show="isOnlineFriends">
|
||||
<friend-item
|
||||
v-for="friend in onlineFriendsByGroupStatus"
|
||||
:key="friend.id"
|
||||
:friend="friend"
|
||||
@confirm-delete-friend="confirmDeleteFriend" />
|
||||
</div>
|
||||
<div
|
||||
v-show="activeFriends.length"
|
||||
class="x-friend-group cursor-pointer flex items-center"
|
||||
@click="
|
||||
isActiveFriends = !isActiveFriends;
|
||||
saveFriendsGroupStates();
|
||||
">
|
||||
<ChevronDown class="rotation-transition" :class="{ 'is-rotated': !isActiveFriends }" />
|
||||
<span style="margin-left: 5px">{{ t('side_panel.active') }} ― {{ activeFriends.length }}</span>
|
||||
</div>
|
||||
<div v-if="isActiveFriends">
|
||||
<friend-item
|
||||
v-for="friend in activeFriends"
|
||||
:key="friend.id"
|
||||
:friend="friend"
|
||||
@confirm-delete-friend="confirmDeleteFriend"></friend-item>
|
||||
</div>
|
||||
<div
|
||||
v-show="offlineFriends.length"
|
||||
class="x-friend-group cursor-pointer flex items-center"
|
||||
@click="
|
||||
isOfflineFriends = !isOfflineFriends;
|
||||
saveFriendsGroupStates();
|
||||
">
|
||||
<ChevronDown class="rotation-transition" :class="{ 'is-rotated': !isOfflineFriends }" />
|
||||
<span style="margin-left: 5px">{{ t('side_panel.offline') }} ― {{ offlineFriends.length }}</span>
|
||||
</div>
|
||||
<div v-if="isOfflineFriends">
|
||||
<friend-item
|
||||
v-for="friend in offlineFriends"
|
||||
:key="friend.id"
|
||||
:friend="friend"
|
||||
@confirm-delete-friend="confirmDeleteFriend"></friend-item>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||
import { ChevronDown } from 'lucide-vue-next';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useVirtualizer } from '@tanstack/vue-virtual';
|
||||
|
||||
import {
|
||||
useAdvancedSettingsStore,
|
||||
@@ -180,9 +99,9 @@
|
||||
} from '../../../stores';
|
||||
import { isRealInstance, userImage, userStatusClass } from '../../../shared/utils';
|
||||
import { getFriendsLocations } from '../../../shared/utils/location.js';
|
||||
import { watchState } from '../../../service/watchState';
|
||||
|
||||
import FriendItem from './FriendItem.vue';
|
||||
import Location from '../../../components/Location.vue';
|
||||
import configRepository from '../../../service/config';
|
||||
|
||||
const emit = defineEmits(['confirm-delete-friend']);
|
||||
@@ -203,18 +122,11 @@
|
||||
const isFriendsGroupMe = ref(true);
|
||||
const isVIPFriends = ref(true);
|
||||
const isOnlineFriends = ref(true);
|
||||
const isActiveFriends = ref(false);
|
||||
const isOfflineFriends = ref(false);
|
||||
const isActiveFriends = ref(true);
|
||||
const isOfflineFriends = ref(true);
|
||||
const isSidebarGroupByInstanceCollapsed = ref(false);
|
||||
|
||||
watch(
|
||||
() => watchState.isFriendsLoaded,
|
||||
(isFriendsLoaded) => {
|
||||
if (isFriendsLoaded) {
|
||||
isOfflineFriends.value = offlineFriends.value.length < 10 ? true : false;
|
||||
}
|
||||
}
|
||||
);
|
||||
const listRootRef = ref(null);
|
||||
const scrollViewportRef = ref(null);
|
||||
|
||||
loadFriendsGroupStates();
|
||||
|
||||
@@ -281,6 +193,235 @@
|
||||
: vipFriendsByGroupStatus.value.length;
|
||||
});
|
||||
|
||||
const buildToggleRow = ({
|
||||
key,
|
||||
label,
|
||||
count = null,
|
||||
expanded = true,
|
||||
headerPadding = null,
|
||||
paddingBottom = null,
|
||||
onClick = null
|
||||
}) => ({
|
||||
type: 'toggle-header',
|
||||
key,
|
||||
label,
|
||||
count,
|
||||
expanded,
|
||||
headerPadding,
|
||||
paddingBottom,
|
||||
onClick
|
||||
});
|
||||
const buildFriendRow = (friend, key, options = {}) => ({
|
||||
type: 'friend-item',
|
||||
key,
|
||||
friend,
|
||||
isGroupByInstance: options.isGroupByInstance,
|
||||
paddingBottom: options.paddingBottom,
|
||||
itemStyle: options.itemStyle
|
||||
});
|
||||
const buildVipSubheaderRow = (label, count, key) => ({
|
||||
type: 'vip-subheader',
|
||||
key,
|
||||
label,
|
||||
count,
|
||||
paddingBottom: 4
|
||||
});
|
||||
const buildInstanceHeaderRow = (location, count, key) => ({
|
||||
type: 'instance-header',
|
||||
key,
|
||||
location,
|
||||
count,
|
||||
paddingBottom: 4
|
||||
});
|
||||
|
||||
const virtualRows = computed(() => {
|
||||
const rows = [];
|
||||
|
||||
rows.push(
|
||||
buildToggleRow({
|
||||
key: 'me-header',
|
||||
label: t('side_panel.me'),
|
||||
expanded: isFriendsGroupMe.value,
|
||||
headerPadding: '0 0 5px',
|
||||
onClick: toggleFriendsGroupMe
|
||||
})
|
||||
);
|
||||
|
||||
if (isFriendsGroupMe.value) {
|
||||
rows.push({ type: 'me-item', key: `me:${currentUser.value?.id ?? 'me'}` });
|
||||
}
|
||||
|
||||
if (vipFriendsDisplayNumber.value) {
|
||||
rows.push(
|
||||
buildToggleRow({
|
||||
key: 'vip-header',
|
||||
label: t('side_panel.favorite'),
|
||||
count: vipFriendsDisplayNumber.value,
|
||||
expanded: isVIPFriends.value,
|
||||
onClick: toggleVIPFriends
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (isVIPFriends.value) {
|
||||
if (isSidebarDivideByFriendGroup.value) {
|
||||
vipFriendsDivideByGroup.value.forEach((group, groupIndex) => {
|
||||
const groupName = group?.[0]?.groupName ?? '';
|
||||
const groupKey = group?.[0]?.key ?? groupIndex;
|
||||
if (groupName) {
|
||||
rows.push(buildVipSubheaderRow(groupName, group.length, `vip-subheader:${groupKey}`));
|
||||
}
|
||||
group.forEach((friend, idx) => {
|
||||
rows.push(
|
||||
buildFriendRow(friend, `vip:${groupKey}:${friend?.id ?? idx}`, {
|
||||
paddingBottom: idx === group.length - 1 ? 10 : undefined
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
vipFriendsByGroupStatus.value.forEach((friend, idx) => {
|
||||
rows.push(buildFriendRow(friend, `vip:${friend?.id ?? idx}`));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (isSidebarGroupByInstance.value && friendsInSameInstance.value.length) {
|
||||
rows.push(
|
||||
buildToggleRow({
|
||||
key: 'same-instance-header',
|
||||
label: t('side_panel.same_instance'),
|
||||
count: friendsInSameInstance.value.length,
|
||||
expanded: !isSidebarGroupByInstanceCollapsed.value,
|
||||
onClick: toggleSwitchGroupByInstanceCollapsed,
|
||||
paddingBottom: 4
|
||||
})
|
||||
);
|
||||
|
||||
if (!isSidebarGroupByInstanceCollapsed.value) {
|
||||
friendsInSameInstance.value.forEach((friendArr, groupIndex) => {
|
||||
if (!friendArr || !friendArr.length) return;
|
||||
const groupKey = friendArr?.[0]?.ref?.$location?.tag ?? `group-${groupIndex}`;
|
||||
rows.push(
|
||||
buildInstanceHeaderRow(getFriendsLocations(friendArr), friendArr.length, `instance:${groupKey}`)
|
||||
);
|
||||
friendArr.forEach((friend, idx) => {
|
||||
rows.push(
|
||||
buildFriendRow(friend, `instance:${groupKey}:${friend?.id ?? idx}`, {
|
||||
isGroupByInstance: true,
|
||||
paddingBottom: idx === friendArr.length - 1 ? 5 : undefined,
|
||||
itemStyle: idx === friendArr.length - 1 ? { marginBottom: '5px' } : undefined
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (onlineFriendsByGroupStatus.value.length) {
|
||||
rows.push(
|
||||
buildToggleRow({
|
||||
key: 'online-header',
|
||||
label: t('side_panel.online'),
|
||||
count: onlineFriendsByGroupStatus.value.length,
|
||||
expanded: isOnlineFriends.value,
|
||||
onClick: toggleOnlineFriends
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (isOnlineFriends.value) {
|
||||
onlineFriendsByGroupStatus.value.forEach((friend, idx) => {
|
||||
rows.push(buildFriendRow(friend, `online:${friend?.id ?? idx}`));
|
||||
});
|
||||
}
|
||||
|
||||
if (activeFriends.value.length) {
|
||||
rows.push(
|
||||
buildToggleRow({
|
||||
key: 'active-header',
|
||||
label: t('side_panel.active'),
|
||||
count: activeFriends.value.length,
|
||||
expanded: isActiveFriends.value,
|
||||
onClick: toggleActiveFriends
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (isActiveFriends.value) {
|
||||
activeFriends.value.forEach((friend, idx) => {
|
||||
rows.push(buildFriendRow(friend, `active:${friend?.id ?? idx}`));
|
||||
});
|
||||
}
|
||||
|
||||
if (offlineFriends.value.length) {
|
||||
rows.push(
|
||||
buildToggleRow({
|
||||
key: 'offline-header',
|
||||
label: t('side_panel.offline'),
|
||||
count: offlineFriends.value.length,
|
||||
expanded: isOfflineFriends.value,
|
||||
onClick: toggleOfflineFriends
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (isOfflineFriends.value) {
|
||||
offlineFriends.value.forEach((friend, idx) => {
|
||||
rows.push(buildFriendRow(friend, `offline:${friend?.id ?? idx}`));
|
||||
});
|
||||
}
|
||||
|
||||
return rows;
|
||||
});
|
||||
|
||||
const estimateRowSize = (row) => {
|
||||
if (!row) {
|
||||
return 44;
|
||||
}
|
||||
if (row.type === 'toggle-header') {
|
||||
return 28 + (row.paddingBottom || 0);
|
||||
}
|
||||
if (row.type === 'vip-subheader') {
|
||||
return 24 + (row.paddingBottom || 0);
|
||||
}
|
||||
if (row.type === 'instance-header') {
|
||||
return 26 + (row.paddingBottom || 0);
|
||||
}
|
||||
return 52 + (row.paddingBottom || 0);
|
||||
};
|
||||
|
||||
const virtualizer = useVirtualizer(
|
||||
computed(() => ({
|
||||
count: virtualRows.value.length,
|
||||
getScrollElement: () => scrollViewportRef.value,
|
||||
estimateSize: (index) => estimateRowSize(virtualRows.value[index]),
|
||||
getItemKey: (index) => virtualRows.value[index]?.key ?? index,
|
||||
overscan: 6
|
||||
}))
|
||||
);
|
||||
|
||||
const virtualItems = computed(() => {
|
||||
const items = virtualizer.value?.getVirtualItems?.() ?? [];
|
||||
return items.map((virtualItem) => ({
|
||||
virtualItem,
|
||||
row: virtualRows.value[virtualItem.index]
|
||||
}));
|
||||
});
|
||||
|
||||
const virtualContainerStyle = computed(() => ({
|
||||
height: `${virtualizer.value?.getTotalSize?.() ?? 0}px`,
|
||||
width: '100%'
|
||||
}));
|
||||
|
||||
const rowStyle = (item) => {
|
||||
const paddingBottom = item?.row?.paddingBottom;
|
||||
return {
|
||||
transform: `translateY(${item.virtualItem.start}px)`,
|
||||
...(paddingBottom ? { paddingBottom: `${paddingBottom}px` } : {})
|
||||
};
|
||||
};
|
||||
|
||||
function saveFriendsGroupStates() {
|
||||
configRepository.setBool('VRCX_isFriendsGroupMe', isFriendsGroupMe.value);
|
||||
configRepository.setBool('VRCX_isFriendsGroupFavorites', isVIPFriends.value);
|
||||
@@ -304,9 +445,47 @@
|
||||
configRepository.setBool('VRCX_sidebarGroupByInstanceCollapsed', isSidebarGroupByInstanceCollapsed.value);
|
||||
}
|
||||
|
||||
function toggleFriendsGroupMe() {
|
||||
isFriendsGroupMe.value = !isFriendsGroupMe.value;
|
||||
saveFriendsGroupStates();
|
||||
}
|
||||
|
||||
function toggleVIPFriends() {
|
||||
isVIPFriends.value = !isVIPFriends.value;
|
||||
saveFriendsGroupStates();
|
||||
}
|
||||
|
||||
function toggleOnlineFriends() {
|
||||
isOnlineFriends.value = !isOnlineFriends.value;
|
||||
saveFriendsGroupStates();
|
||||
}
|
||||
|
||||
function toggleActiveFriends() {
|
||||
isActiveFriends.value = !isActiveFriends.value;
|
||||
saveFriendsGroupStates();
|
||||
}
|
||||
|
||||
function toggleOfflineFriends() {
|
||||
isOfflineFriends.value = !isOfflineFriends.value;
|
||||
saveFriendsGroupStates();
|
||||
}
|
||||
|
||||
function confirmDeleteFriend(friend) {
|
||||
emit('confirm-delete-friend', friend);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
scrollViewportRef.value = listRootRef.value?.closest('[data-slot="scroll-area-viewport"]') ?? null;
|
||||
nextTick(() => {
|
||||
virtualizer.value?.measure?.();
|
||||
});
|
||||
});
|
||||
|
||||
watch(virtualRows, () => {
|
||||
nextTick(() => {
|
||||
virtualizer.value?.measure?.();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -316,4 +495,23 @@
|
||||
.rotation-transition {
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.friend-sidebar__virtual {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.friend-sidebar__virtual-row {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.friend-sidebar__virtual-row--toggle-header .x-friend-group {
|
||||
padding: 16px 0 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user