919 lines
31 KiB
Vue
919 lines
31 KiB
Vue
<script setup>
|
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
import { useRoute, useRouter } from 'vue-router';
|
|
import { useI18n } from 'vue-i18n';
|
|
import { useAuthStore } from '@/features/auth/stores/authStore.js';
|
|
import { useChannelsStore } from '@/features/channels/stores/channelsStore.js';
|
|
import { useNotificationsStore } from '@/features/notifications/stores/notificationsStore.js';
|
|
import { getNotificationRoute } from '@/features/notifications/notificationRoutes.js';
|
|
import { useContentItemsStore } from '@/features/content/stores/contentItemsStore.js';
|
|
import { useCampaignsStore } from '@/features/campaigns/stores/campaignsStore.js';
|
|
import SidebarUserMenu from './SidebarUserMenu.vue';
|
|
import {
|
|
mdiBellOutline,
|
|
mdiCalendarMonthOutline,
|
|
mdiChevronDown,
|
|
mdiFolderOutline,
|
|
mdiHomeOutline,
|
|
mdiImageMultipleOutline,
|
|
mdiLan,
|
|
mdiMagnify,
|
|
mdiPlus,
|
|
mdiBugOutline,
|
|
} from '@mdi/js';
|
|
|
|
const props = defineProps({
|
|
isExpanded: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
});
|
|
|
|
const router = useRouter();
|
|
const { t } = useI18n();
|
|
const route = useRoute();
|
|
const authStore = useAuthStore();
|
|
const channelsStore = useChannelsStore();
|
|
const contentItemsStore = useContentItemsStore();
|
|
const notificationsStore = useNotificationsStore();
|
|
const campaignsStore = useCampaignsStore();
|
|
const isNotificationsOpen = ref(false);
|
|
const isSearchFocused = ref(false);
|
|
const searchQuery = ref('');
|
|
|
|
const notificationsRef = ref(null);
|
|
const searchRef = ref(null);
|
|
const collapsedSearchInputRef = ref(null);
|
|
const collapsedSearchPanelStyle = ref({});
|
|
|
|
const primaryLinks = [
|
|
{ to: '/app/dashboard', labelKey: 'nav.overview', icon: mdiHomeOutline },
|
|
{ to: '/app/media-library', labelKey: 'nav.mediaLibrary', icon: mdiImageMultipleOutline },
|
|
{ to: '/app/feedback', labelKey: 'nav.feedbackReview', icon: mdiBugOutline, roles: ['developer'] },
|
|
];
|
|
const visiblePrimaryLinks = computed(() =>
|
|
primaryLinks.filter(link => !link.roles || authStore.hasAnyRole(link.roles))
|
|
);
|
|
|
|
const openSections = ref({
|
|
channels: false,
|
|
campaigns: false,
|
|
});
|
|
|
|
const normalizedSearchQuery = computed(() => searchQuery.value.trim().toLowerCase());
|
|
const campaignResults = computed(() => {
|
|
if (!normalizedSearchQuery.value) {
|
|
return [];
|
|
}
|
|
|
|
return campaignsStore.campaigns
|
|
.filter(campaign => campaign.name.toLowerCase().includes(normalizedSearchQuery.value))
|
|
.slice(0, 5)
|
|
.map(campaign => ({
|
|
id: campaign.id,
|
|
label: campaign.name,
|
|
description: 'Campaign',
|
|
route: { name: 'campaign-detail', params: { campaignId: campaign.id } },
|
|
}));
|
|
});
|
|
const contentResults = computed(() => {
|
|
if (!normalizedSearchQuery.value) {
|
|
return [];
|
|
}
|
|
|
|
return contentItemsStore.items
|
|
.filter(item => {
|
|
const titleMatch = item.title.toLowerCase().includes(normalizedSearchQuery.value);
|
|
const hashtagMatch = (item.hashtags ?? '').toLowerCase().includes(normalizedSearchQuery.value);
|
|
return titleMatch || hashtagMatch;
|
|
})
|
|
.slice(0, 6)
|
|
.map(item => ({
|
|
id: item.id,
|
|
label: item.title,
|
|
description: item.hashtags || item.publicationTargets,
|
|
route: { name: 'content-item-detail', params: { id: item.id } },
|
|
}));
|
|
});
|
|
const hasSearchResults = computed(() =>
|
|
campaignResults.value.length > 0 || contentResults.value.length > 0
|
|
);
|
|
const isSearchOpen = computed(() => isSearchFocused.value && normalizedSearchQuery.value.length > 0);
|
|
const isSearchPanelOpen = computed(() =>
|
|
isSearchFocused.value && (!props.isExpanded || normalizedSearchQuery.value.length > 0)
|
|
);
|
|
|
|
const notificationTitleMap = computed(() => ({
|
|
'approval.requested': t('notifications.events.approvalRequested'),
|
|
'approval.decision.recorded': t('notifications.events.approvalDecisionRecorded'),
|
|
'comment.created': t('notifications.events.commentCreated'),
|
|
'comment.resolved': t('notifications.events.commentResolved'),
|
|
'content-item.created': t('notifications.events.contentCreated'),
|
|
'content-item.revision.created': t('notifications.events.revisionCreated'),
|
|
'content-item.status.updated': t('notifications.events.statusUpdated'),
|
|
'asset.google-drive-linked': t('notifications.events.assetLinked'),
|
|
'asset.revision.created': t('notifications.events.assetRevisionCreated'),
|
|
'Feedback.ReportCreated': t('notifications.events.feedbackReportCreated'),
|
|
'Feedback.DeveloperCommented': t('notifications.events.feedbackDeveloperCommented'),
|
|
'Feedback.StatusChanged': t('notifications.events.feedbackStatusChanged'),
|
|
'Feedback.ReporterCommented': t('notifications.events.feedbackReporterCommented'),
|
|
}));
|
|
|
|
function toggleSection(sectionName) {
|
|
openSections.value[sectionName] = !openSections.value[sectionName];
|
|
}
|
|
|
|
function toggleNotifications() {
|
|
isNotificationsOpen.value = !isNotificationsOpen.value;
|
|
}
|
|
|
|
function updateCollapsedSearchPanelPosition() {
|
|
if (props.isExpanded || !searchRef.value) {
|
|
collapsedSearchPanelStyle.value = {};
|
|
return;
|
|
}
|
|
|
|
const rect = searchRef.value.getBoundingClientRect();
|
|
const left = rect.right + 12;
|
|
const availableWidth = Math.max(240, window.innerWidth - left - 12);
|
|
|
|
collapsedSearchPanelStyle.value = {
|
|
left: `${left}px`,
|
|
top: `${Math.max(12, rect.top)}px`,
|
|
width: `${Math.min(352, availableWidth)}px`,
|
|
};
|
|
}
|
|
|
|
async function openCollapsedSearch() {
|
|
if (props.isExpanded) {
|
|
return;
|
|
}
|
|
|
|
isSearchFocused.value = true;
|
|
updateCollapsedSearchPanelPosition();
|
|
await nextTick();
|
|
collapsedSearchInputRef.value?.focus();
|
|
}
|
|
|
|
function formatNotificationTitle(notification) {
|
|
return notificationTitleMap.value[notification.eventType] ?? notification.message;
|
|
}
|
|
|
|
function formatNotificationDate(value) {
|
|
if (!value) {
|
|
return '';
|
|
}
|
|
|
|
return new Date(value).toLocaleString();
|
|
}
|
|
|
|
async function openNotification(notification) {
|
|
if (!notification.readAt) {
|
|
await notificationsStore.markAsRead(notification.id);
|
|
}
|
|
|
|
isNotificationsOpen.value = false;
|
|
|
|
const notificationRoute = getNotificationRoute(notification, authStore);
|
|
if (notificationRoute) {
|
|
await router.push(notificationRoute);
|
|
}
|
|
}
|
|
|
|
async function openSearchResult(result) {
|
|
isSearchFocused.value = false;
|
|
await router.push(result.route);
|
|
}
|
|
|
|
function handleDocumentClick(event) {
|
|
if (searchRef.value && !searchRef.value.contains(event.target)) {
|
|
isSearchFocused.value = false;
|
|
}
|
|
|
|
if (isNotificationsOpen.value && notificationsRef.value && !notificationsRef.value.contains(event.target)) {
|
|
isNotificationsOpen.value = false;
|
|
}
|
|
}
|
|
|
|
watch(
|
|
() => route.path,
|
|
path => {
|
|
if (path.startsWith('/app/channels')) {
|
|
openSections.value.channels = true;
|
|
}
|
|
|
|
if (path.startsWith('/app/campaigns')) {
|
|
openSections.value.campaigns = true;
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
);
|
|
|
|
watch(isSearchPanelOpen, isOpen => {
|
|
if (isOpen) {
|
|
updateCollapsedSearchPanelPosition();
|
|
}
|
|
});
|
|
|
|
watch(
|
|
() => props.isExpanded,
|
|
isExpanded => {
|
|
if (isExpanded) {
|
|
collapsedSearchPanelStyle.value = {};
|
|
return;
|
|
}
|
|
|
|
isNotificationsOpen.value = false;
|
|
updateCollapsedSearchPanelPosition();
|
|
}
|
|
);
|
|
|
|
onMounted(() => {
|
|
document.addEventListener('click', handleDocumentClick);
|
|
window.addEventListener('resize', updateCollapsedSearchPanelPosition);
|
|
window.addEventListener('scroll', updateCollapsedSearchPanelPosition, true);
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
document.removeEventListener('click', handleDocumentClick);
|
|
window.removeEventListener('resize', updateCollapsedSearchPanelPosition);
|
|
window.removeEventListener('scroll', updateCollapsedSearchPanelPosition, true);
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<aside
|
|
class="app-sidebar"
|
|
:class="{ 'app-sidebar-collapsed': !isExpanded }"
|
|
>
|
|
|
|
<div class="brand-block">
|
|
<router-link
|
|
class="brand-link"
|
|
:class="{ 'brand-link-collapsed': !isExpanded }"
|
|
to="/"
|
|
>
|
|
<span class="brand-mark">S</span>
|
|
<div v-if="isExpanded">
|
|
<div class="brand-name">Socialize</div>
|
|
<div class="brand-caption">{{ t('nav.brandCaption') }}</div>
|
|
</div>
|
|
</router-link>
|
|
</div>
|
|
|
|
<div class="app-sidebar-scroll">
|
|
<div
|
|
v-if="authStore.isAuthenticated"
|
|
class="sidebar-section sidebar-utilities"
|
|
>
|
|
<div
|
|
ref="searchRef"
|
|
class="sidebar-search-wrap"
|
|
>
|
|
<label
|
|
class="sidebar-search"
|
|
:class="{ 'sidebar-search-open': isSearchOpen }"
|
|
:title="!isExpanded ? 'Search' : null"
|
|
@click="openCollapsedSearch"
|
|
>
|
|
<v-icon
|
|
:icon="mdiMagnify"
|
|
class="sidebar-search-icon"
|
|
/>
|
|
<input
|
|
v-if="isExpanded"
|
|
v-model="searchQuery"
|
|
type="search"
|
|
class="sidebar-search-input"
|
|
placeholder="Search"
|
|
@focus="isSearchFocused = true"
|
|
/>
|
|
</label>
|
|
|
|
<div
|
|
v-if="isSearchPanelOpen"
|
|
class="sidebar-floating-panel"
|
|
:class="{ 'sidebar-search-panel-collapsed': !isExpanded }"
|
|
:style="!isExpanded ? collapsedSearchPanelStyle : null"
|
|
>
|
|
<label
|
|
v-if="!isExpanded"
|
|
class="sidebar-search sidebar-search-panel-input"
|
|
>
|
|
<v-icon
|
|
:icon="mdiMagnify"
|
|
class="sidebar-search-icon"
|
|
/>
|
|
<input
|
|
ref="collapsedSearchInputRef"
|
|
v-model="searchQuery"
|
|
type="search"
|
|
class="sidebar-search-input"
|
|
placeholder="Search"
|
|
/>
|
|
</label>
|
|
|
|
<div
|
|
v-if="campaignResults.length"
|
|
class="sidebar-search-group"
|
|
>
|
|
<strong>Campaigns</strong>
|
|
<button
|
|
v-for="result in campaignResults"
|
|
:key="`campaign-${result.id}`"
|
|
class="sidebar-search-result"
|
|
@click="openSearchResult(result)"
|
|
>
|
|
<span>{{ result.label }}</span>
|
|
<small>{{ result.description }}</small>
|
|
</button>
|
|
</div>
|
|
|
|
<div
|
|
v-if="contentResults.length"
|
|
class="sidebar-search-group"
|
|
>
|
|
<strong>Content items</strong>
|
|
<button
|
|
v-for="result in contentResults"
|
|
:key="`content-${result.id}`"
|
|
class="sidebar-search-result"
|
|
@click="openSearchResult(result)"
|
|
>
|
|
<span>{{ result.label }}</span>
|
|
<small>{{ result.description }}</small>
|
|
</button>
|
|
</div>
|
|
|
|
<div
|
|
v-if="normalizedSearchQuery.length > 0 && !hasSearchResults"
|
|
class="sidebar-search-empty"
|
|
>
|
|
No results found.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
ref="notificationsRef"
|
|
class="sidebar-notifications-wrap"
|
|
>
|
|
<button
|
|
class="sidebar-link sidebar-utility-link"
|
|
type="button"
|
|
@click.stop="toggleNotifications"
|
|
>
|
|
<span class="sidebar-link-main">
|
|
<span class="sidebar-notification-icon-wrap">
|
|
<v-icon :icon="mdiBellOutline" />
|
|
<span
|
|
v-if="notificationsStore.unreadCount"
|
|
class="sidebar-notification-badge"
|
|
>
|
|
{{ Math.min(notificationsStore.unreadCount, 9) }}
|
|
</span>
|
|
</span>
|
|
<span
|
|
v-if="isExpanded"
|
|
class="sidebar-link-label"
|
|
>
|
|
{{ t('notifications.title') }}
|
|
</span>
|
|
</span>
|
|
</button>
|
|
|
|
<div
|
|
v-if="isExpanded && isNotificationsOpen"
|
|
class="sidebar-floating-panel sidebar-notifications-panel"
|
|
>
|
|
<div class="sidebar-notifications-header">
|
|
<strong>{{ t('notifications.title') }}</strong>
|
|
<span>{{ notificationsStore.unreadCount }} {{ t('notifications.unread') }}</span>
|
|
</div>
|
|
|
|
<div
|
|
v-if="notificationsStore.isLoading"
|
|
class="sidebar-notifications-empty"
|
|
>
|
|
{{ t('notifications.loading') }}
|
|
</div>
|
|
|
|
<div
|
|
v-else-if="notificationsStore.error"
|
|
class="sidebar-notifications-empty"
|
|
>
|
|
{{ notificationsStore.error }}
|
|
</div>
|
|
|
|
<button
|
|
v-for="notification in notificationsStore.recentItems"
|
|
:key="notification.id"
|
|
class="sidebar-notification-row"
|
|
:class="{ 'sidebar-notification-row-unread': !notification.readAt }"
|
|
@click="openNotification(notification)"
|
|
>
|
|
<strong>{{ formatNotificationTitle(notification) }}</strong>
|
|
<span>{{ notification.message }}</span>
|
|
<small>{{ formatNotificationDate(notification.createdAt) }}</small>
|
|
</button>
|
|
|
|
<div
|
|
v-if="!notificationsStore.isLoading && !notificationsStore.recentItems.length"
|
|
class="sidebar-notifications-empty"
|
|
>
|
|
{{ t('notifications.empty') }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="sidebar-section">
|
|
<router-link
|
|
v-for="link in visiblePrimaryLinks"
|
|
:key="link.to"
|
|
:to="link.to"
|
|
class="sidebar-link"
|
|
active-class="sidebar-link-active"
|
|
:title="!isExpanded ? t(link.labelKey) : null"
|
|
>
|
|
<v-icon :icon="link.icon" />
|
|
<span
|
|
v-if="isExpanded"
|
|
class="sidebar-link-label"
|
|
>
|
|
{{ t(link.labelKey) }}
|
|
</span>
|
|
</router-link>
|
|
</div>
|
|
|
|
<div class="sidebar-section">
|
|
<div class="sidebar-section-header">
|
|
<router-link
|
|
to="/app/content"
|
|
class="sidebar-link sidebar-link-section"
|
|
active-class="sidebar-link-active"
|
|
:title="!isExpanded ? t('nav.content') : null"
|
|
>
|
|
<span class="sidebar-link-main">
|
|
<v-icon :icon="mdiCalendarMonthOutline" />
|
|
<span
|
|
v-if="isExpanded"
|
|
class="sidebar-link-label"
|
|
>
|
|
{{ t('nav.content') }}
|
|
</span>
|
|
</span>
|
|
</router-link>
|
|
|
|
<router-link
|
|
v-if="isExpanded"
|
|
:to="{ name: 'content-item-create' }"
|
|
class="sidebar-section-action"
|
|
:title="t('contentItems.newItem')"
|
|
>
|
|
<v-icon :icon="mdiPlus" />
|
|
</router-link>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="sidebar-section">
|
|
<div class="sidebar-section-header">
|
|
<router-link
|
|
to="/app/campaigns"
|
|
class="sidebar-link sidebar-link-section"
|
|
active-class="sidebar-link-active"
|
|
:title="!isExpanded ? t('nav.campaigns') : null"
|
|
@click="toggleSection('campaigns')"
|
|
>
|
|
<span class="sidebar-link-main">
|
|
<v-icon :icon="mdiFolderOutline" />
|
|
<span
|
|
v-if="isExpanded"
|
|
class="sidebar-link-label"
|
|
>
|
|
{{ t('nav.campaigns') }}
|
|
</span>
|
|
<v-icon
|
|
v-if="isExpanded"
|
|
:icon="mdiChevronDown"
|
|
class="sidebar-chevron"
|
|
:class="{ 'sidebar-chevron-open': openSections.campaigns }"
|
|
/>
|
|
</span>
|
|
</router-link>
|
|
|
|
<router-link
|
|
v-if="isExpanded"
|
|
to="/app/campaigns?create=true"
|
|
class="sidebar-section-action"
|
|
:title="t('campaigns.createTitle')"
|
|
>
|
|
<v-icon :icon="mdiPlus" />
|
|
</router-link>
|
|
</div>
|
|
|
|
<div
|
|
v-if="isExpanded && openSections.campaigns"
|
|
class="sidebar-sublist"
|
|
>
|
|
<router-link
|
|
to="/app/campaigns"
|
|
class="sidebar-sublink sidebar-sublink-overview"
|
|
active-class="sidebar-sublink-active"
|
|
>
|
|
<span>{{ t('sidebar.allCampaigns') }}</span>
|
|
</router-link>
|
|
|
|
<router-link
|
|
v-for="campaign in campaignsStore.campaigns"
|
|
:key="campaign.id"
|
|
:to="{ name: 'campaign-detail', params: { campaignId: campaign.id } }"
|
|
class="sidebar-sublink"
|
|
active-class="sidebar-sublink-active"
|
|
>
|
|
<span>{{ campaign.name }}</span>
|
|
</router-link>
|
|
|
|
<div
|
|
v-if="!campaignsStore.campaigns.length"
|
|
class="sidebar-empty"
|
|
>
|
|
{{ t('sidebar.noCampaigns') }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="sidebar-section">
|
|
<div class="sidebar-section-header">
|
|
<router-link
|
|
to="/app/channels"
|
|
class="sidebar-link sidebar-link-section"
|
|
active-class="sidebar-link-active"
|
|
:title="!isExpanded ? t('nav.channels') : null"
|
|
@click="toggleSection('channels')"
|
|
>
|
|
<span class="sidebar-link-main">
|
|
<v-icon :icon="mdiLan" />
|
|
<span
|
|
v-if="isExpanded"
|
|
class="sidebar-link-label"
|
|
>
|
|
{{ t('nav.channels') }}
|
|
</span>
|
|
<v-icon
|
|
v-if="isExpanded"
|
|
:icon="mdiChevronDown"
|
|
class="sidebar-chevron"
|
|
:class="{ 'sidebar-chevron-open': openSections.channels }"
|
|
/>
|
|
</span>
|
|
</router-link>
|
|
|
|
<router-link
|
|
v-if="isExpanded"
|
|
to="/app/channels?create=true"
|
|
class="sidebar-section-action"
|
|
:title="t('channels.createTitle')"
|
|
>
|
|
<v-icon :icon="mdiPlus" />
|
|
</router-link>
|
|
</div>
|
|
|
|
<div
|
|
v-if="isExpanded && openSections.channels"
|
|
class="sidebar-sublist"
|
|
>
|
|
<router-link
|
|
to="/app/channels"
|
|
class="sidebar-sublink sidebar-sublink-overview"
|
|
active-class="sidebar-sublink-active"
|
|
>
|
|
<span>{{ t('sidebar.allChannels') }}</span>
|
|
</router-link>
|
|
|
|
<router-link
|
|
v-for="channel in channelsStore.channels"
|
|
:key="channel.id"
|
|
:to="{ name: 'channels', query: { channel: channel.id } }"
|
|
class="sidebar-sublink"
|
|
active-class="sidebar-sublink-active"
|
|
>
|
|
<span>{{ channel.name }}</span>
|
|
</router-link>
|
|
|
|
<div
|
|
v-if="!channelsStore.channels.length"
|
|
class="sidebar-empty"
|
|
>
|
|
{{ t('sidebar.noChannels') }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<SidebarUserMenu
|
|
v-if="authStore.isAuthenticated"
|
|
:is-expanded="isExpanded"
|
|
/>
|
|
</aside>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.app-sidebar {
|
|
@apply flex h-full w-[19rem] flex-shrink-0 flex-col px-4 pt-4 transition-[width,padding] duration-200;
|
|
border-right: 1px solid rgba(23, 32, 51, 0.08);
|
|
}
|
|
|
|
.app-sidebar-scroll {
|
|
@apply flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto pb-4 pt-4;
|
|
}
|
|
|
|
.brand-block {
|
|
@apply flex items-center gap-3 pb-4;
|
|
border-bottom: 1px solid rgba(23, 32, 51, 0.08);
|
|
}
|
|
|
|
.brand-link {
|
|
@apply flex min-w-0 items-center gap-3 no-underline;
|
|
color: inherit;
|
|
}
|
|
|
|
.brand-link-collapsed {
|
|
@apply w-full justify-center;
|
|
}
|
|
|
|
.brand-mark {
|
|
@apply flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-[1.1rem] text-xl font-black;
|
|
background: linear-gradient(135deg, #ff8a3d 0%, #ef4444 100%);
|
|
color: #fffaf2;
|
|
}
|
|
|
|
.brand-name {
|
|
@apply text-lg font-black uppercase tracking-[0.18em];
|
|
color: #172033;
|
|
}
|
|
|
|
.brand-caption {
|
|
@apply text-xs uppercase tracking-[0.24em];
|
|
color: #5d6b82;
|
|
}
|
|
|
|
.side-menu {
|
|
@apply flex flex-1 items-center justify-between gap-3;
|
|
}
|
|
|
|
.sidebar-utilities {
|
|
@apply gap-3 pb-1;
|
|
}
|
|
|
|
.sidebar-search-wrap,
|
|
.sidebar-notifications-wrap {
|
|
@apply relative;
|
|
}
|
|
|
|
.sidebar-search {
|
|
@apply flex h-11 items-center gap-3 rounded-[1.1rem] border px-4 transition-colors;
|
|
background: rgba(23, 32, 51, 0.04);
|
|
border-color: rgba(23, 32, 51, 0.06);
|
|
color: #526178;
|
|
}
|
|
|
|
.sidebar-search-open,
|
|
.sidebar-search:focus-within {
|
|
background: rgba(255, 255, 255, 0.96);
|
|
border-color: rgba(23, 32, 51, 0.1);
|
|
}
|
|
|
|
.sidebar-search-icon {
|
|
@apply h-5 w-5 flex-shrink-0 text-xl;
|
|
}
|
|
|
|
.sidebar-search-input {
|
|
@apply min-w-0 flex-1 border-0 bg-transparent p-0 text-sm;
|
|
color: #172033;
|
|
outline: none;
|
|
}
|
|
|
|
.sidebar-search-input::placeholder {
|
|
color: #7a8799;
|
|
}
|
|
|
|
.sidebar-floating-panel {
|
|
@apply absolute left-0 right-0 top-[calc(100%+0.6rem)] z-40 flex flex-col gap-3 rounded-[1.25rem] border p-3;
|
|
background: rgba(255, 255, 255, 0.98);
|
|
border-color: rgba(23, 32, 51, 0.08);
|
|
box-shadow: 0 18px 40px rgba(23, 32, 51, 0.12);
|
|
}
|
|
|
|
.sidebar-search-panel-collapsed {
|
|
@apply fixed right-auto top-auto;
|
|
}
|
|
|
|
.sidebar-search-panel-input {
|
|
@apply bg-white;
|
|
}
|
|
|
|
.sidebar-search-group {
|
|
@apply flex flex-col gap-1;
|
|
}
|
|
|
|
.sidebar-search-group strong {
|
|
@apply px-2 text-xs font-black uppercase tracking-[0.18em];
|
|
color: #5d6b82;
|
|
}
|
|
|
|
.sidebar-search-result {
|
|
@apply flex flex-col gap-1 rounded-[0.95rem] px-3 py-3 text-left transition-colors;
|
|
color: #172033;
|
|
}
|
|
|
|
.sidebar-search-result:hover {
|
|
background: rgba(23, 32, 51, 0.06);
|
|
}
|
|
|
|
.sidebar-search-result small,
|
|
.sidebar-search-empty {
|
|
@apply text-xs leading-5;
|
|
color: #526178;
|
|
}
|
|
|
|
.sidebar-search-empty {
|
|
@apply px-2 py-1;
|
|
}
|
|
|
|
.sidebar-utility-link {
|
|
@apply w-full justify-between text-left;
|
|
}
|
|
|
|
.sidebar-notification-icon-wrap {
|
|
@apply relative flex items-center justify-center;
|
|
}
|
|
|
|
.sidebar-notification-badge {
|
|
@apply absolute -right-2 -top-2 flex h-5 min-w-[1.25rem] items-center justify-center rounded-full px-1 text-[10px] font-black;
|
|
background: #ef4444;
|
|
color: #fffaf2;
|
|
}
|
|
|
|
.sidebar-notifications-panel {
|
|
@apply gap-1;
|
|
}
|
|
|
|
.sidebar-notifications-header {
|
|
@apply mb-1 flex items-center justify-between gap-3 px-3 py-2;
|
|
}
|
|
|
|
.sidebar-notifications-header strong {
|
|
@apply text-sm font-black;
|
|
color: #172033;
|
|
}
|
|
|
|
.sidebar-notifications-header span,
|
|
.sidebar-notifications-empty,
|
|
.sidebar-notification-row span,
|
|
.sidebar-notification-row small {
|
|
@apply text-xs leading-5;
|
|
color: #526178;
|
|
}
|
|
|
|
.sidebar-notifications-empty {
|
|
@apply px-3 py-4;
|
|
}
|
|
|
|
.sidebar-notification-row {
|
|
@apply flex flex-col gap-1 rounded-[0.9rem] px-3 py-3 text-left transition-colors;
|
|
}
|
|
|
|
.sidebar-notification-row:hover {
|
|
background: rgba(23, 32, 51, 0.06);
|
|
}
|
|
|
|
.sidebar-notification-row-unread {
|
|
background: rgba(15, 118, 110, 0.08);
|
|
}
|
|
|
|
.sidebar-notification-row strong {
|
|
@apply text-sm font-semibold;
|
|
color: #172033;
|
|
}
|
|
|
|
.sidebar-section {
|
|
@apply flex flex-col gap-2;
|
|
}
|
|
|
|
.sidebar-section-header {
|
|
@apply flex items-center gap-2;
|
|
}
|
|
|
|
.sidebar-link {
|
|
@apply flex min-w-0 items-center gap-3 rounded-[1.1rem] px-4 py-3 text-sm font-semibold no-underline transition-colors;
|
|
color: #44516a;
|
|
}
|
|
|
|
.sidebar-link:hover {
|
|
background: rgba(23, 32, 51, 0.06);
|
|
color: #172033;
|
|
}
|
|
|
|
.sidebar-link-active {
|
|
background: linear-gradient(135deg, rgba(255, 138, 61, 0.14), rgba(239, 68, 68, 0.1));
|
|
color: #172033;
|
|
box-shadow: inset 0 0 0 1px rgba(255, 138, 61, 0.2);
|
|
}
|
|
|
|
.sidebar-link-section {
|
|
@apply flex-1;
|
|
}
|
|
|
|
.sidebar-link-main {
|
|
@apply flex min-w-0 items-center gap-3;
|
|
}
|
|
|
|
.sidebar-link-label {
|
|
@apply truncate;
|
|
}
|
|
|
|
.sidebar-section-action {
|
|
@apply ml-auto flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-[1rem] transition-colors no-underline;
|
|
color: #526178;
|
|
}
|
|
|
|
.sidebar-section-action:hover {
|
|
background: rgba(23, 32, 51, 0.06);
|
|
color: #172033;
|
|
}
|
|
|
|
.sidebar-chevron {
|
|
@apply text-base transition-transform;
|
|
}
|
|
|
|
.sidebar-chevron-open {
|
|
transform: rotate(180deg);
|
|
}
|
|
|
|
.sidebar-link :deep(.v-icon),
|
|
.sidebar-section-action :deep(.v-icon) {
|
|
@apply h-5 w-5 flex-shrink-0 text-xl;
|
|
}
|
|
|
|
.sidebar-sublist {
|
|
@apply flex flex-col gap-1 pl-4;
|
|
}
|
|
|
|
.sidebar-sublink {
|
|
@apply flex flex-col rounded-[1rem] px-4 py-3 text-sm no-underline transition-colors;
|
|
color: #526178;
|
|
}
|
|
|
|
.sidebar-sublink:hover,
|
|
.sidebar-sublink-active {
|
|
background: rgba(23, 32, 51, 0.05);
|
|
color: #172033;
|
|
}
|
|
|
|
.sidebar-sublink strong {
|
|
@apply font-semibold;
|
|
}
|
|
|
|
.sidebar-sublink small,
|
|
.sidebar-empty {
|
|
@apply text-xs;
|
|
color: #7a8799;
|
|
}
|
|
|
|
.app-sidebar-collapsed {
|
|
@apply w-[5.5rem] px-3;
|
|
}
|
|
|
|
.app-sidebar-collapsed .brand-block {
|
|
@apply justify-center;
|
|
}
|
|
|
|
.app-sidebar-collapsed .sidebar-search {
|
|
@apply justify-center px-0;
|
|
}
|
|
|
|
.app-sidebar-collapsed .sidebar-link {
|
|
@apply justify-center px-0;
|
|
}
|
|
|
|
.app-sidebar-collapsed .sidebar-link-main {
|
|
@apply justify-center;
|
|
}
|
|
|
|
.app-sidebar-collapsed .sidebar-search:hover {
|
|
background: rgba(23, 32, 51, 0.07);
|
|
}
|
|
|
|
.app-sidebar-collapsed .sidebar-search-panel-input {
|
|
@apply justify-start px-4;
|
|
}
|
|
|
|
.app-sidebar-collapsed .sidebar-floating-panel {
|
|
left: calc(100% + 0.75rem);
|
|
right: auto;
|
|
width: min(22rem, calc(100vw - 7rem));
|
|
}
|
|
</style>
|