feat: just getting better and better
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
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';
|
||||
@@ -13,8 +13,6 @@
|
||||
mdiBellOutline,
|
||||
mdiCalendarMonthOutline,
|
||||
mdiChevronDown,
|
||||
mdiCogOutline,
|
||||
mdiFileDocumentOutline,
|
||||
mdiFolderOutline,
|
||||
mdiHomeOutline,
|
||||
mdiImageMultipleOutline,
|
||||
@@ -45,14 +43,13 @@
|
||||
|
||||
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/workspace', labelKey: 'nav.workspacePlan', icon: mdiCalendarMonthOutline },
|
||||
{ to: '/app/media-library', labelKey: 'nav.mediaLibrary', icon: mdiImageMultipleOutline },
|
||||
{ to: '/app/my-feedback', labelKey: 'nav.myFeedback', icon: mdiBugOutline },
|
||||
{ to: '/app/feedback', labelKey: 'nav.feedbackReview', icon: mdiBugOutline, roles: ['developer'] },
|
||||
{ to: '/app/workspace-settings', labelKey: 'nav.settings', icon: mdiCogOutline },
|
||||
];
|
||||
const visiblePrimaryLinks = computed(() =>
|
||||
primaryLinks.filter(link => !link.roles || authStore.hasAnyRole(link.roles))
|
||||
@@ -102,6 +99,9 @@
|
||||
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'),
|
||||
@@ -127,6 +127,34 @@
|
||||
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;
|
||||
}
|
||||
@@ -181,12 +209,35 @@
|
||||
{ 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>
|
||||
|
||||
@@ -222,6 +273,8 @@
|
||||
<label
|
||||
class="sidebar-search"
|
||||
:class="{ 'sidebar-search-open': isSearchOpen }"
|
||||
:title="!isExpanded ? 'Search' : null"
|
||||
@click="openCollapsedSearch"
|
||||
>
|
||||
<v-icon
|
||||
:icon="mdiMagnify"
|
||||
@@ -238,9 +291,28 @@
|
||||
</label>
|
||||
|
||||
<div
|
||||
v-if="isSearchOpen"
|
||||
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"
|
||||
@@ -274,7 +346,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!hasSearchResults"
|
||||
v-if="normalizedSearchQuery.length > 0 && !hasSearchResults"
|
||||
class="sidebar-search-empty"
|
||||
>
|
||||
No results found.
|
||||
@@ -383,7 +455,7 @@
|
||||
:title="!isExpanded ? t('nav.content') : null"
|
||||
>
|
||||
<span class="sidebar-link-main">
|
||||
<v-icon :icon="mdiFileDocumentOutline" />
|
||||
<v-icon :icon="mdiCalendarMonthOutline" />
|
||||
<span
|
||||
v-if="isExpanded"
|
||||
class="sidebar-link-label"
|
||||
@@ -411,6 +483,7 @@
|
||||
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" />
|
||||
@@ -420,6 +493,12 @@
|
||||
>
|
||||
{{ t('nav.campaigns') }}
|
||||
</span>
|
||||
<v-icon
|
||||
v-if="isExpanded"
|
||||
:icon="mdiChevronDown"
|
||||
class="sidebar-chevron"
|
||||
:class="{ 'sidebar-chevron-open': openSections.campaigns }"
|
||||
/>
|
||||
</span>
|
||||
</router-link>
|
||||
|
||||
@@ -431,19 +510,6 @@
|
||||
>
|
||||
<v-icon :icon="mdiPlus" />
|
||||
</router-link>
|
||||
|
||||
<button
|
||||
v-if="isExpanded"
|
||||
class="sidebar-section-toggle"
|
||||
type="button"
|
||||
@click="toggleSection('campaigns')"
|
||||
>
|
||||
<v-icon
|
||||
:icon="mdiChevronDown"
|
||||
class="sidebar-chevron"
|
||||
:class="{ 'sidebar-chevron-open': openSections.campaigns }"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -484,6 +550,7 @@
|
||||
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" />
|
||||
@@ -493,6 +560,12 @@
|
||||
>
|
||||
{{ t('nav.channels') }}
|
||||
</span>
|
||||
<v-icon
|
||||
v-if="isExpanded"
|
||||
:icon="mdiChevronDown"
|
||||
class="sidebar-chevron"
|
||||
:class="{ 'sidebar-chevron-open': openSections.channels }"
|
||||
/>
|
||||
</span>
|
||||
</router-link>
|
||||
|
||||
@@ -504,19 +577,6 @@
|
||||
>
|
||||
<v-icon :icon="mdiPlus" />
|
||||
</router-link>
|
||||
|
||||
<button
|
||||
v-if="isExpanded"
|
||||
class="sidebar-section-toggle"
|
||||
type="button"
|
||||
@click="toggleSection('channels')"
|
||||
>
|
||||
<v-icon
|
||||
:icon="mdiChevronDown"
|
||||
class="sidebar-chevron"
|
||||
:class="{ 'sidebar-chevron-open': openSections.channels }"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -566,20 +626,25 @@
|
||||
}
|
||||
|
||||
.app-sidebar-scroll {
|
||||
@apply flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto pb-4 pt-3 pr-3;
|
||||
@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;
|
||||
@apply flex items-center gap-3 pb-4;
|
||||
border-bottom: 1px solid rgba(23, 32, 51, 0.08);
|
||||
}
|
||||
|
||||
.brand-link {
|
||||
@apply flex items-center gap-3 no-underline;
|
||||
@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 items-center justify-center rounded-2xl text-lg font-black;
|
||||
@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;
|
||||
}
|
||||
@@ -621,7 +686,7 @@
|
||||
}
|
||||
|
||||
.sidebar-search-icon {
|
||||
@apply text-xl;
|
||||
@apply h-5 w-5 flex-shrink-0 text-xl;
|
||||
}
|
||||
|
||||
.sidebar-search-input {
|
||||
@@ -641,6 +706,14 @@
|
||||
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;
|
||||
}
|
||||
@@ -761,18 +834,12 @@
|
||||
@apply truncate;
|
||||
}
|
||||
|
||||
.sidebar-section-toggle {
|
||||
@apply flex h-11 w-11 items-center justify-center rounded-[1rem] transition-colors;
|
||||
color: #526178;
|
||||
}
|
||||
|
||||
.sidebar-section-action {
|
||||
@apply flex h-11 w-11 items-center justify-center rounded-[1rem] transition-colors no-underline;
|
||||
@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,
|
||||
.sidebar-section-toggle:hover {
|
||||
.sidebar-section-action:hover {
|
||||
background: rgba(23, 32, 51, 0.06);
|
||||
color: #172033;
|
||||
}
|
||||
@@ -785,8 +852,9 @@
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.sidebar-link :deep(.v-icon) {
|
||||
@apply text-xl;
|
||||
.sidebar-link :deep(.v-icon),
|
||||
.sidebar-section-action :deep(.v-icon) {
|
||||
@apply h-5 w-5 flex-shrink-0 text-xl;
|
||||
}
|
||||
|
||||
.sidebar-sublist {
|
||||
@@ -818,10 +886,30 @@
|
||||
@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;
|
||||
|
||||
Reference in New Issue
Block a user