1374 lines
36 KiB
Vue
1374 lines
36 KiB
Vue
<!--
|
||||
|
|
Copyright 2024-2026 ChatterMate
|
|||
|
|
|
|||
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|||
|
|
you may not use this file except in compliance with the License.
|
|||
|
|
You may obtain a copy of the License at
|
|||
|
|
|
|||
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|||
|
|
|
|||
|
|
Unless required by applicable law or agreed to in writing, software
|
|||
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|||
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
|
|
See the License for the specific language governing permissions and
|
|||
|
|
limitations under the License.
|
|||
|
|
-->
|
|||
|
|
|
|||
|
|
<script setup lang="ts">
|
|||
|
|
import { ref, onMounted, watch, computed } from 'vue'
|
|||
|
|
import { useAuth } from '@/composables/useAuth'
|
|||
|
|
import { useTheme } from '@/composables/useTheme'
|
|||
|
|
import AppSidebar from '@/components/layout/AppSidebar.vue'
|
|||
|
|
import BottomNav from '@/components/layout/BottomNav.vue'
|
|||
|
|
import MoreSheet from '@/components/layout/MoreSheet.vue'
|
|||
|
|
import { navIconSvg } from '@/components/layout/navIcons'
|
|||
|
|
import userAvatar from '@/assets/user.svg'
|
|||
|
|
import notificationIcon from '@/assets/notification.svg'
|
|||
|
|
import NotificationList from '@/components/notifications/NotificationList.vue'
|
|||
|
|
import EnablePushPrompt from '@/components/notifications/EnablePushPrompt.vue'
|
|||
|
|
import { userService } from '@/services/user'
|
|||
|
|
import { permissionChecks } from '@/utils/permissions'
|
|||
|
|
import type { User } from '@/types/user'
|
|||
|
|
import { useNotifications } from '@/composables/useNotifications'
|
|||
|
|
import { notificationService } from '@/services/notification'
|
|||
|
|
import { useRoute, useRouter } from 'vue-router'
|
|||
|
|
import { updateUserStatus } from '@/services/users'
|
|||
|
|
import { useEnterpriseFeatures } from '@/composables/useEnterpriseFeatures'
|
|||
|
|
import { myAvatarUrl } from '@/config/api'
|
|||
|
|
import { useBreakpoint } from '@/composables/useBreakpoint'
|
|||
|
|
|
|||
|
|
const props = defineProps<{
|
|||
|
|
hideSidebar?: boolean
|
|||
|
|
hideHeader?: boolean
|
|||
|
|
}>()
|
|||
|
|
|
|||
|
|
// Initialize sidebar state based on current route and screen size
|
|||
|
|
const route = useRoute()
|
|||
|
|
const { isMobile: isPhone, isTablet: isMobile } = useBreakpoint()
|
|||
|
|
const isSidebarOpen = ref(
|
|||
|
|
route.path !== '/conversations' && !isMobile.value
|
|||
|
|
)
|
|||
|
|
const showUserMenu = ref(false)
|
|||
|
|
const showNotifications = ref(false)
|
|||
|
|
const showMoreSheet = ref(false)
|
|||
|
|
const currentUser = ref<User>(userService.getCurrentUser() as User)
|
|||
|
|
const userName = ref(userService.getUserName())
|
|||
|
|
const userRole = ref(userService.getUserRole())
|
|||
|
|
const unreadCount = ref(0)
|
|||
|
|
const statusUpdating = ref(false)
|
|||
|
|
const { logout } = useAuth()
|
|||
|
|
const { mode: themeMode, toggle: toggleTheme } = useTheme()
|
|||
|
|
const themeTitle = computed(() =>
|
|||
|
|
themeMode.value === 'dark' ? '主题:深色 — 点击切换为浅色'
|
|||
|
|
: themeMode.value === 'light' ? '主题:浅色 — 点击切换为跟随系统'
|
|||
|
|
: '主题:跟随系统 — 点击切换为深色'
|
|||
|
|
)
|
|||
|
|
// Attaches push listeners when permission is already granted; the permission
|
|||
|
|
// request itself only happens from the EnablePushPrompt user gesture.
|
|||
|
|
const { enableNotifications } = useNotifications()
|
|||
|
|
const router = useRouter()
|
|||
|
|
|
|||
|
|
const PAGE_TITLES: Record<string, string> = {
|
|||
|
|
'/ai-agents': 'AI 客服',
|
|||
|
|
'/human-agents': '人工客服',
|
|||
|
|
'/conversations': '会话',
|
|||
|
|
'/analytics': '数据分析',
|
|||
|
|
'/settings/organization': '组织',
|
|||
|
|
'/settings/subscription': '订阅',
|
|||
|
|
'/settings/integrations': '集成',
|
|||
|
|
'/settings/app-integrations': '应用集成',
|
|||
|
|
'/settings/widget-apps': '网站组件',
|
|||
|
|
'/settings/ai-config': 'AI 配置',
|
|||
|
|
'/settings/user': '个人设置',
|
|||
|
|
}
|
|||
|
|
const pageTitle = computed(() => PAGE_TITLES[route.path] || '')
|
|||
|
|
|
|||
|
|
// Initialize enterprise features
|
|||
|
|
const { hasEnterpriseModule, subscriptionStore, initializeSubscriptionStore, showMessageLimitWarning, messageLimitStatus, enterpriseComponent, moduleImports } = useEnterpriseFeatures()
|
|||
|
|
// Promo strip / usage nudge / post-signup popup (enterprise only; empty in OSS)
|
|||
|
|
const PromoSurfaces = enterpriseComponent(moduleImports.promoSurfaces)
|
|||
|
|
// "Finish setup" bar until the org's agent is live (enterprise only; empty in OSS)
|
|||
|
|
const SetupProgressBar = enterpriseComponent(moduleImports.setupProgressBar)
|
|||
|
|
// Always-on offer pill in the header: survives the strip being dismissed
|
|||
|
|
const PromoHeaderPill = enterpriseComponent(moduleImports.promoHeaderPill)
|
|||
|
|
// Help button opening the hosted support chat (empty in OSS)
|
|||
|
|
const SupportChatButton = enterpriseComponent(moduleImports.supportChatButton)
|
|||
|
|
|
|||
|
|
const currentPlan = computed(() => subscriptionStore.value.currentPlan)
|
|||
|
|
const isLoadingPlan = computed(() => subscriptionStore.value.isLoadingPlan)
|
|||
|
|
const isInTrial = computed(() => subscriptionStore.value.isInTrial)
|
|||
|
|
const trialDaysLeft = computed(() => subscriptionStore.value.trialDaysLeft)
|
|||
|
|
|
|||
|
|
const userAvatarSrc = computed(() => {
|
|||
|
|
if (!currentUser.value?.profile_pic) return userAvatar
|
|||
|
|
// Never the stored URL: the copy we hold was signed at login and expires an
|
|||
|
|
// hour later. Ask the API, which signs on the spot. Cache-busted so a freshly
|
|||
|
|
// uploaded picture replaces the one the browser already cached.
|
|||
|
|
return myAvatarUrl(new Date().getTime())
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const toggleOnlineStatus = async () => {
|
|||
|
|
if (statusUpdating.value) return
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
statusUpdating.value = true
|
|||
|
|
const newStatus = !currentUser.value?.is_online
|
|||
|
|
await updateUserStatus(currentUser.value?.id as string, newStatus)
|
|||
|
|
|
|||
|
|
// Update local state
|
|||
|
|
currentUser.value = {
|
|||
|
|
...currentUser.value,
|
|||
|
|
is_online: newStatus,
|
|||
|
|
last_seen: new Date().toISOString()
|
|||
|
|
} as User
|
|||
|
|
|
|||
|
|
userService.setCurrentUser(currentUser.value as User)
|
|||
|
|
showUserMenu.value = false
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Failed to update status:', error)
|
|||
|
|
} finally {
|
|||
|
|
statusUpdating.value = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Bottom nav shows on phones only; hidden in fullscreen workflows and on the
|
|||
|
|
// full-screen chat pane (mobile chat detail = /conversations with ?session=)
|
|||
|
|
const showBottomNav = computed(() =>
|
|||
|
|
isPhone.value &&
|
|||
|
|
!props.hideSidebar &&
|
|||
|
|
!(route.path === '/conversations' && route.query.session)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// Watch for route changes to update sidebar state and close menus
|
|||
|
|
watch(
|
|||
|
|
() => route.path,
|
|||
|
|
(newPath) => {
|
|||
|
|
showUserMenu.value = false
|
|||
|
|
showNotifications.value = false
|
|||
|
|
showMoreSheet.value = false
|
|||
|
|
|
|||
|
|
// Set sidebar state based on route and screen size
|
|||
|
|
if (newPath === '/conversations') {
|
|||
|
|
isSidebarOpen.value = false // Collapsed for conversations
|
|||
|
|
} else if (isMobile.value) {
|
|||
|
|
isSidebarOpen.value = false // Collapsed on mobile by default
|
|||
|
|
} else {
|
|||
|
|
isSidebarOpen.value = true // Expanded for all other routes on desktop
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
const fetchUnreadCount = async () => {
|
|||
|
|
try {
|
|||
|
|
unreadCount.value = await notificationService.getUnreadCount()
|
|||
|
|
} catch (err) {
|
|||
|
|
console.error('Error fetching unread count:', err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// React to breakpoint changes (was a manual resize listener)
|
|||
|
|
watch(isMobile, (mobile) => {
|
|||
|
|
if (mobile && isSidebarOpen.value) {
|
|||
|
|
isSidebarOpen.value = false
|
|||
|
|
} else if (!mobile && !isSidebarOpen.value && route.path !== '/conversations') {
|
|||
|
|
isSidebarOpen.value = true
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
onMounted(() => {
|
|||
|
|
fetchUnreadCount()
|
|||
|
|
|
|||
|
|
if (hasEnterpriseModule) {
|
|||
|
|
initializeSubscriptionStore().then(() => {
|
|||
|
|
subscriptionStore.value.fetchCurrentPlan().then(() => {
|
|||
|
|
}).catch((err: Error) => {
|
|||
|
|
console.error('Error fetching current plan:', err)
|
|||
|
|
})
|
|||
|
|
}).catch((err: Error) => {
|
|||
|
|
console.error('Error initializing subscription store:', err)
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const toggleSidebar = () => {
|
|||
|
|
isSidebarOpen.value = !isSidebarOpen.value
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const closeSidebar = () => {
|
|||
|
|
// On mobile, close the sidebar when clicking backdrop
|
|||
|
|
if (isMobile.value) {
|
|||
|
|
isSidebarOpen.value = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const navigateToUpgrade = () => {
|
|||
|
|
router.push('/settings/subscription')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// The usage warning is for everyone — an agent hitting a message ceiling needs
|
|||
|
|
// to know why replies stop. The two remedies are not: both lead to pages a
|
|||
|
|
// non-admin cannot open, so they rendered as doors into a 403.
|
|||
|
|
const canManageSubscription = permissionChecks.canManageSubscription()
|
|||
|
|
const canViewAIConfig = permissionChecks.canViewAIConfig()
|
|||
|
|
|
|||
|
|
// Computed for layout classes
|
|||
|
|
const layoutClasses = computed(() => ({
|
|||
|
|
'sidebar-collapsed': !isSidebarOpen.value || props.hideSidebar,
|
|||
|
|
'header-hidden': props.hideHeader,
|
|||
|
|
'fullscreen-workflow': props.hideSidebar && props.hideHeader,
|
|||
|
|
'has-bottom-nav': showBottomNav.value
|
|||
|
|
}))
|
|||
|
|
|
|||
|
|
const openNotificationsFromSheet = () => {
|
|||
|
|
showMoreSheet.value = false
|
|||
|
|
showNotifications.value = true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<template>
|
|||
|
|
<div class="dashboard-layout" :class="layoutClasses">
|
|||
|
|
<!-- Backdrop for mobile -->
|
|||
|
|
<div
|
|||
|
|
v-if="!props.hideSidebar && isSidebarOpen"
|
|||
|
|
class="sidebar-backdrop"
|
|||
|
|
@click="closeSidebar"
|
|||
|
|
></div>
|
|||
|
|
|
|||
|
|
<AppSidebar
|
|||
|
|
v-if="!props.hideSidebar"
|
|||
|
|
:isCollapsed="!isSidebarOpen"
|
|||
|
|
@toggle="toggleSidebar"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<!-- Main Content -->
|
|||
|
|
<div class="main-content">
|
|||
|
|
<!-- Message Limit Warning Banner -->
|
|||
|
|
<div v-if="!props.hideHeader && hasEnterpriseModule && showMessageLimitWarning && messageLimitStatus"
|
|||
|
|
class="message-limit-banner"
|
|||
|
|
:class="messageLimitStatus.type">
|
|||
|
|
<div class="banner-content">
|
|||
|
|
<div class="banner-text">
|
|||
|
|
<span class="banner-icon" v-if="messageLimitStatus.type === 'error'">⚠️</span>
|
|||
|
|
<span class="banner-icon" v-else>ℹ️</span>
|
|||
|
|
{{ messageLimitStatus.message }}
|
|||
|
|
</div>
|
|||
|
|
<div v-if="canManageSubscription || canViewAIConfig" class="banner-actions">
|
|||
|
|
<button
|
|||
|
|
v-if="canManageSubscription"
|
|||
|
|
class="action-button"
|
|||
|
|
@click="navigateToUpgrade"
|
|||
|
|
>
|
|||
|
|
升级套餐
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
v-if="canViewAIConfig"
|
|||
|
|
class="action-button secondary"
|
|||
|
|
@click="router.push('/settings/ai-config')"
|
|||
|
|
>
|
|||
|
|
配置自有模型
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="usage-bar">
|
|||
|
|
<div class="usage-progress"
|
|||
|
|
:style="{ width: `${Math.min(messageLimitStatus.percentage, 100)}%` }"
|
|||
|
|
:class="{ 'exceeded': messageLimitStatus.percentage >= 100 }">
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- Promo offer surfaces (strip, usage nudge, first-landing popup) -->
|
|||
|
|
<PromoSurfaces
|
|||
|
|
v-if="!props.hideHeader && hasEnterpriseModule"
|
|||
|
|
:hide-usage-card="showMessageLimitWarning"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<!-- Header -->
|
|||
|
|
<header v-if="!props.hideHeader" class="header">
|
|||
|
|
<div class="header-content">
|
|||
|
|
<div class="left-section">
|
|||
|
|
<!-- Hamburger menu for mobile -->
|
|||
|
|
<button class="hamburger-menu" @click="toggleSidebar" aria-label="切换菜单">
|
|||
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|||
|
|
<path d="M3 12H21M3 6H21M3 18H21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|||
|
|
</svg>
|
|||
|
|
</button>
|
|||
|
|
<h1 v-if="pageTitle" class="topbar-page-title">{{ pageTitle }}</h1>
|
|||
|
|
</div>
|
|||
|
|
<div class="right-section">
|
|||
|
|
<button class="icon-btn" @click="toggleTheme" :title="themeTitle" :aria-label="themeTitle"
|
|||
|
|
v-html="navIconSvg(themeMode === 'dark' ? 'moon' : themeMode === 'light' ? 'sun' : 'monitor', 17)">
|
|||
|
|
</button>
|
|||
|
|
<SupportChatButton v-if="hasEnterpriseModule" />
|
|||
|
|
<PromoHeaderPill v-if="hasEnterpriseModule" />
|
|||
|
|
<div v-if="hasEnterpriseModule && (isLoadingPlan || isInTrial)" class="plan-display">
|
|||
|
|
<div v-if="isLoadingPlan" class="plan-loading">
|
|||
|
|
<span class="loading-spinner"></span>
|
|||
|
|
加载中…
|
|||
|
|
</div>
|
|||
|
|
<div v-else-if="isInTrial" class="trial-info">
|
|||
|
|
<span
|
|||
|
|
class="trial-badge"
|
|||
|
|
:class="{ clickable: canManageSubscription }"
|
|||
|
|
@click="canManageSubscription && navigateToUpgrade()"
|
|||
|
|
>
|
|||
|
|
试用(剩余 {{ trialDaysLeft }} 天)
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="user-menu">
|
|||
|
|
<button class="notification-button" @click="showNotifications = !showNotifications">
|
|||
|
|
<img :src="notificationIcon" alt="通知" class="notification-icon" />
|
|||
|
|
<span v-if="unreadCount > 0" class="notification-badge">
|
|||
|
|
{{ unreadCount > 99 ? '99+' : unreadCount }}
|
|||
|
|
</span>
|
|||
|
|
</button>
|
|||
|
|
|
|||
|
|
<div class="topbar-divider" aria-hidden="true"></div>
|
|||
|
|
|
|||
|
|
<div class="user-profile">
|
|||
|
|
<div class="profile-trigger" @click="showUserMenu = !showUserMenu">
|
|||
|
|
<div class="avatar-wrapper">
|
|||
|
|
<img :src="userAvatarSrc" alt="用户" class="avatar" />
|
|||
|
|
<span
|
|||
|
|
class="status-indicator"
|
|||
|
|
:class="{ 'online': currentUser?.is_online }"
|
|||
|
|
></span>
|
|||
|
|
</div>
|
|||
|
|
<div class="user-info" v-if="isSidebarOpen">
|
|||
|
|
<span class="name">{{ userName }}</span>
|
|||
|
|
<div class="plan-info">
|
|||
|
|
<span class="plan-badge" :class="currentPlan?.plan?.type">
|
|||
|
|
<span class="plan-icon">⚡</span>
|
|||
|
|
{{ currentPlan?.plan?.name || '' }}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="dropdown-menu" v-if="showUserMenu">
|
|||
|
|
<div class="status-menu-item">
|
|||
|
|
<span class="status-label">状态:</span>
|
|||
|
|
<button
|
|||
|
|
class="status-toggle"
|
|||
|
|
:class="{ 'online': currentUser?.is_online }"
|
|||
|
|
@click="toggleOnlineStatus"
|
|||
|
|
:disabled="statusUpdating"
|
|||
|
|
>
|
|||
|
|
{{ currentUser?.is_online ? '在线' : '离线' }}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="menu-divider"></div>
|
|||
|
|
|
|||
|
|
<button class="menu-item" @click="logout">退出登录</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</header>
|
|||
|
|
|
|||
|
|
<!-- Main Content Area -->
|
|||
|
|
<main class="content">
|
|||
|
|
<!-- The agents page shows its own copy, beside the wizard and list -->
|
|||
|
|
<SetupProgressBar v-if="route.name !== 'ai-agents'" />
|
|||
|
|
<slot></slot>
|
|||
|
|
</main>
|
|||
|
|
|
|||
|
|
<!-- Footer: omitted for full-page layouts (hideHeader, e.g. ConversationsView) —
|
|||
|
|
those pages size .content to a fixed 100vh, so a footer still taking up
|
|||
|
|
real space at the bottom clips the last message / message input out of view. -->
|
|||
|
|
<footer v-if="!props.hideHeader" class="footer">
|
|||
|
|
<div class="footer-content">
|
|||
|
|
<p>© 2024 ChatterMate。保留所有权利。</p>
|
|||
|
|
<nav class="footer-links">
|
|||
|
|
<a href="https://chattermate.chat/privacy_policy.html">隐私政策</a>
|
|||
|
|
<a href="https://chattermate.chat/terms_and_conditions.html">服务条款</a>
|
|||
|
|
</nav>
|
|||
|
|
</div>
|
|||
|
|
</footer>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- Notification drawer (fixed) — outside the header so the More sheet
|
|||
|
|
can open it on pages that hide the header (e.g. Inbox) -->
|
|||
|
|
<NotificationList :is-open="showNotifications" @close="showNotifications = false"
|
|||
|
|
@notification-read="fetchUnreadCount" />
|
|||
|
|
|
|||
|
|
<EnablePushPrompt @enable="enableNotifications" />
|
|||
|
|
|
|||
|
|
<!-- Mobile app shell -->
|
|||
|
|
<BottomNav
|
|||
|
|
v-if="showBottomNav"
|
|||
|
|
:unread-count="unreadCount"
|
|||
|
|
:more-open="showMoreSheet"
|
|||
|
|
@more="showMoreSheet = true"
|
|||
|
|
/>
|
|||
|
|
<MoreSheet
|
|||
|
|
:open="showMoreSheet"
|
|||
|
|
:is-online="currentUser?.is_online"
|
|||
|
|
:status-updating="statusUpdating"
|
|||
|
|
:theme-mode="themeMode"
|
|||
|
|
:unread-count="unreadCount"
|
|||
|
|
@close="showMoreSheet = false"
|
|||
|
|
@toggle-status="toggleOnlineStatus"
|
|||
|
|
@toggle-theme="toggleTheme"
|
|||
|
|
@notifications="openNotificationsFromSheet"
|
|||
|
|
@logout="logout"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.dashboard-layout {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: auto 1fr;
|
|||
|
|
height: 100vh;
|
|||
|
|
height: 100dvh;
|
|||
|
|
transition: grid-template-columns var(--transition-normal);
|
|||
|
|
overflow: hidden;
|
|||
|
|
width: 100%;
|
|||
|
|
position: relative;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.sidebar-backdrop {
|
|||
|
|
display: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Sidebar Styles */
|
|||
|
|
.sidebar {
|
|||
|
|
background: var(--background-soft);
|
|||
|
|
border-right: 1px solid var(--border-color);
|
|||
|
|
padding: var(--space-md);
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: var(--space-lg);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.sidebar-header {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
padding-bottom: var(--space-md);
|
|||
|
|
border-bottom: 1px solid var(--border-color);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.logo {
|
|||
|
|
height: 32px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.toggle-btn {
|
|||
|
|
background: none;
|
|||
|
|
border: none;
|
|||
|
|
color: var(--text-color);
|
|||
|
|
cursor: pointer;
|
|||
|
|
opacity: 0.7;
|
|||
|
|
transition: var(--transition-fast);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.toggle-btn:hover {
|
|||
|
|
opacity: 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.nav-item {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: var(--space-sm);
|
|||
|
|
padding: var(--space-sm);
|
|||
|
|
color: var(--text-color);
|
|||
|
|
text-decoration: none;
|
|||
|
|
border-radius: var(--radius-sm);
|
|||
|
|
transition: var(--transition-fast);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.nav-item:hover {
|
|||
|
|
background: var(--background-mute);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.nav-item.active {
|
|||
|
|
background: var(--accent-solid);
|
|||
|
|
color: var(--background-color);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Header Styles */
|
|||
|
|
.header {
|
|||
|
|
background: var(--topbar);
|
|||
|
|
backdrop-filter: blur(12px);
|
|||
|
|
-webkit-backdrop-filter: blur(12px);
|
|||
|
|
border-bottom: 1px solid var(--o07);
|
|||
|
|
position: sticky;
|
|||
|
|
top: 0;
|
|||
|
|
z-index: 50;
|
|||
|
|
/* Standalone PWA: content extends under the status bar (black-translucent) */
|
|||
|
|
padding-top: var(--safe-top);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.topbar-page-title {
|
|||
|
|
font-family: var(--font-display);
|
|||
|
|
font-size: 16px;
|
|||
|
|
font-weight: var(--font-weight-semibold);
|
|||
|
|
letter-spacing: -0.01em;
|
|||
|
|
color: var(--text2);
|
|||
|
|
margin: 0;
|
|||
|
|
line-height: 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.icon-btn {
|
|||
|
|
width: 38px;
|
|||
|
|
height: 38px;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
background: var(--o04);
|
|||
|
|
border: 1px solid var(--o10);
|
|||
|
|
border-radius: 10px;
|
|||
|
|
color: var(--text3);
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: background-color var(--transition-fast), color var(--transition-fast);
|
|||
|
|
flex-shrink: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.icon-btn:hover {
|
|||
|
|
background: var(--o08);
|
|||
|
|
color: var(--text);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.header-content {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
padding: var(--space-md) var(--space-xl);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.left-section {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: var(--space-md);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.hamburger-menu {
|
|||
|
|
display: none;
|
|||
|
|
background: none;
|
|||
|
|
border: none;
|
|||
|
|
color: var(--text-color);
|
|||
|
|
cursor: pointer;
|
|||
|
|
padding: var(--space-xs);
|
|||
|
|
border-radius: var(--radius-sm);
|
|||
|
|
transition: var(--transition-fast);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.hamburger-menu:hover {
|
|||
|
|
background: var(--background-mute);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.hamburger-menu svg {
|
|||
|
|
display: block;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.right-section {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 18px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.search input {
|
|||
|
|
background: var(--background-mute);
|
|||
|
|
border: 1px solid var(--border-color);
|
|||
|
|
border-radius: var(--radius-full);
|
|||
|
|
padding: var(--space-sm) var(--space-lg);
|
|||
|
|
color: var(--text-color);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.user-menu {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 18px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.notifications {
|
|||
|
|
background: none;
|
|||
|
|
border: none;
|
|||
|
|
color: var(--text-color);
|
|||
|
|
cursor: pointer;
|
|||
|
|
opacity: 0.7;
|
|||
|
|
transition: var(--transition-fast);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.notifications:hover {
|
|||
|
|
opacity: 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.user-profile {
|
|||
|
|
position: relative;
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.profile-trigger {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: var(--space-sm);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.avatar-wrapper {
|
|||
|
|
position: relative;
|
|||
|
|
width: 40px;
|
|||
|
|
height: 40px;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
background: var(--grad-purple-teal);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.avatar {
|
|||
|
|
width: 40px;
|
|||
|
|
height: 40px;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
object-fit: cover;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.user-info {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.name {
|
|||
|
|
font-weight: 500;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.role {
|
|||
|
|
font-size: var(--text-sm);
|
|||
|
|
opacity: 0.7;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dropdown-menu {
|
|||
|
|
position: absolute;
|
|||
|
|
top: 100%;
|
|||
|
|
right: 0;
|
|||
|
|
background: var(--surface);
|
|||
|
|
border: 1px solid var(--o10);
|
|||
|
|
border-radius: 16px;
|
|||
|
|
padding: var(--space-xs);
|
|||
|
|
margin-top: var(--space-sm);
|
|||
|
|
min-width: 220px;
|
|||
|
|
z-index: 100;
|
|||
|
|
box-shadow: var(--shadow-lg);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.menu-item {
|
|||
|
|
display: block;
|
|||
|
|
width: 100%;
|
|||
|
|
padding: var(--space-sm) var(--space-md);
|
|||
|
|
text-align: left;
|
|||
|
|
background: none;
|
|||
|
|
border: none;
|
|||
|
|
color: var(--text3);
|
|||
|
|
font-size: var(--text-sm);
|
|||
|
|
font-weight: var(--font-weight-medium);
|
|||
|
|
font-family: var(--font-sans);
|
|||
|
|
cursor: pointer;
|
|||
|
|
border-radius: var(--radius-sm);
|
|||
|
|
transition: background-color var(--transition-fast), color var(--transition-fast);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.menu-item:hover {
|
|||
|
|
background: var(--o06);
|
|||
|
|
color: var(--text);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Content Styles */
|
|||
|
|
.content {
|
|||
|
|
padding: var(--space-xl);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Remove padding when header is hidden (for full-page layouts like ConversationsView) */
|
|||
|
|
.dashboard-layout.header-hidden .content {
|
|||
|
|
padding: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Footer Styles */
|
|||
|
|
.footer {
|
|||
|
|
background: var(--bg2);
|
|||
|
|
border-top: 1px solid var(--o07);
|
|||
|
|
padding: var(--space-lg) var(--space-xl);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.footer-content {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.footer-links {
|
|||
|
|
display: flex;
|
|||
|
|
gap: var(--space-lg);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.footer-links a {
|
|||
|
|
color: var(--text-color);
|
|||
|
|
text-decoration: none;
|
|||
|
|
opacity: 0.7;
|
|||
|
|
transition: var(--transition-fast);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.footer-links a:hover {
|
|||
|
|
opacity: 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.notification-button {
|
|||
|
|
width: 38px;
|
|||
|
|
height: 38px;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
background: var(--o04);
|
|||
|
|
border: 1px solid var(--o10);
|
|||
|
|
border-radius: 10px;
|
|||
|
|
color: var(--muted);
|
|||
|
|
padding: 0;
|
|||
|
|
cursor: pointer;
|
|||
|
|
position: relative;
|
|||
|
|
flex-shrink: 0;
|
|||
|
|
transition: background-color var(--transition-fast), color var(--transition-fast);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.notification-button:hover {
|
|||
|
|
background: var(--o08);
|
|||
|
|
color: var(--text);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.notification-icon {
|
|||
|
|
width: 18px;
|
|||
|
|
height: 18px;
|
|||
|
|
filter: var(--icon-filter, brightness(0) invert(1));
|
|||
|
|
opacity: var(--icon-opacity, 0.55);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.notification-badge {
|
|||
|
|
position: absolute;
|
|||
|
|
top: -5px;
|
|||
|
|
right: -5px;
|
|||
|
|
min-width: 17px;
|
|||
|
|
height: 17px;
|
|||
|
|
padding: 0 4px;
|
|||
|
|
background-color: var(--c-danger);
|
|||
|
|
color: #fff;
|
|||
|
|
border: 1.5px solid var(--bg);
|
|||
|
|
border-radius: var(--radius-pill);
|
|||
|
|
font-family: var(--font-display);
|
|||
|
|
font-size: 10px;
|
|||
|
|
font-weight: var(--font-weight-bold);
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.status-indicator {
|
|||
|
|
position: absolute;
|
|||
|
|
bottom: 0;
|
|||
|
|
right: 0;
|
|||
|
|
width: 10px;
|
|||
|
|
height: 10px;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
background-color: var(--muted);
|
|||
|
|
border: 2px solid var(--bg2);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.status-indicator.online {
|
|||
|
|
background-color: var(--success-color);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.topbar-divider {
|
|||
|
|
width: 1px;
|
|||
|
|
height: 26px;
|
|||
|
|
background: var(--o10);
|
|||
|
|
flex-shrink: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.status-menu-item {
|
|||
|
|
padding: var(--space-sm);
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
gap: var(--space-sm);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.status-label {
|
|||
|
|
color: var(--text-muted);
|
|||
|
|
font-size: 0.9rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.status-toggle {
|
|||
|
|
background: var(--background-mute);
|
|||
|
|
border: none;
|
|||
|
|
padding: var(--space-xs) var(--space-sm);
|
|||
|
|
border-radius: var(--radius-sm);
|
|||
|
|
color: var(--text-muted);
|
|||
|
|
cursor: pointer;
|
|||
|
|
font-size: 0.9rem;
|
|||
|
|
transition: all 0.3s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.status-toggle.online {
|
|||
|
|
background: var(--success-color);
|
|||
|
|
color: white;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.status-toggle:disabled {
|
|||
|
|
opacity: 0.7;
|
|||
|
|
cursor: not-allowed;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.menu-divider {
|
|||
|
|
height: 1px;
|
|||
|
|
background: var(--o08);
|
|||
|
|
margin: var(--space-xs) 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.plan-display {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: var(--space-md);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.plan-loading {
|
|||
|
|
color: var(--text-muted);
|
|||
|
|
font-size: var(--text-sm);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.plan-info {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: var(--space-sm);
|
|||
|
|
margin-top: 2px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.plan-badge {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 4px;
|
|||
|
|
padding: 2px 8px;
|
|||
|
|
border-radius: var(--radius-full);
|
|||
|
|
font-size: var(--text-xs);
|
|||
|
|
font-weight: 600;
|
|||
|
|
background: var(--o08);
|
|||
|
|
color: var(--text3);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.plan-badge.pro {
|
|||
|
|
background: linear-gradient(45deg, #000000, #333333);
|
|||
|
|
color: white;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.plan-badge.enterprise {
|
|||
|
|
background: linear-gradient(45deg, var(--accent-solid), var(--accent-solid));
|
|||
|
|
color: white;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.plan-icon {
|
|||
|
|
font-size: var(--text-xs);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.upgrade-button.gradient {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 4px;
|
|||
|
|
padding: 2px 8px;
|
|||
|
|
font-size: var(--text-xs);
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: white;
|
|||
|
|
background: linear-gradient(45deg, var(--accent-solid), var(--accent-solid));
|
|||
|
|
border: none;
|
|||
|
|
border-radius: var(--radius-full);
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: all 0.2s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.upgrade-button.gradient:hover {
|
|||
|
|
transform: translateY(-1px);
|
|||
|
|
filter: brightness(1.1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.upgrade-icon {
|
|||
|
|
font-size: var(--text-xs);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.upgrade-button {
|
|||
|
|
padding: var(--space-xs) var(--space-sm);
|
|||
|
|
border-radius: var(--radius-sm);
|
|||
|
|
background-color: var(--accent-solid);
|
|||
|
|
color: white;
|
|||
|
|
border: none;
|
|||
|
|
font-size: var(--text-sm);
|
|||
|
|
font-weight: 500;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: all 0.2s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.upgrade-button:hover {
|
|||
|
|
background-color: var(--accent-solid);
|
|||
|
|
transform: translateY(-1px);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.trial-info {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
margin-right: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.trial-badge {
|
|||
|
|
background: linear-gradient(135deg, var(--accent-solid), var(--accent-solid));
|
|||
|
|
color: white;
|
|||
|
|
padding: 4px 12px;
|
|||
|
|
border-radius: 12px;
|
|||
|
|
font-size: 0.875rem;
|
|||
|
|
font-weight: 500;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: all 0.2s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.trial-badge:hover {
|
|||
|
|
transform: translateY(-1px);
|
|||
|
|
filter: brightness(1.1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.trial-badge.clickable {
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.message-limit-banner {
|
|||
|
|
position: relative;
|
|||
|
|
padding: var(--space-md) var(--space-xl);
|
|||
|
|
background: var(--warning-bg);
|
|||
|
|
border-bottom: 1px solid var(--warning-color);
|
|||
|
|
width: 100%;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.message-limit-banner.error {
|
|||
|
|
background: var(--error-bg);
|
|||
|
|
border-bottom: 1px solid var(--error-color);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.banner-content {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: var(--space-md);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.banner-text {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: var(--space-sm);
|
|||
|
|
font-weight: 500;
|
|||
|
|
word-break: break-word;
|
|||
|
|
flex: 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.banner-icon {
|
|||
|
|
font-size: 1.2em;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.banner-actions {
|
|||
|
|
display: flex;
|
|||
|
|
gap: var(--space-sm);
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.action-button {
|
|||
|
|
padding: var(--space-xs) var(--space-sm);
|
|||
|
|
border-radius: var(--radius-sm);
|
|||
|
|
font-size: var(--text-sm);
|
|||
|
|
font-weight: 500;
|
|||
|
|
cursor: pointer;
|
|||
|
|
border: none;
|
|||
|
|
background: var(--accent-solid);
|
|||
|
|
color: white;
|
|||
|
|
transition: all 0.2s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.action-button:hover {
|
|||
|
|
transform: translateY(-1px);
|
|||
|
|
filter: brightness(1.1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.action-button.secondary {
|
|||
|
|
background: var(--background-soft);
|
|||
|
|
color: var(--text-color);
|
|||
|
|
border: 1px solid var(--border-color);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.usage-bar {
|
|||
|
|
position: absolute;
|
|||
|
|
bottom: 0;
|
|||
|
|
left: 0;
|
|||
|
|
width: 100%;
|
|||
|
|
height: 2px;
|
|||
|
|
background: var(--background-mute);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.usage-progress {
|
|||
|
|
height: 100%;
|
|||
|
|
background: var(--warning-color);
|
|||
|
|
transition: width 0.3s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.usage-progress.exceeded {
|
|||
|
|
background: var(--error-color);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Adjust main content to account for banner */
|
|||
|
|
.main-content {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
height: 100vh;
|
|||
|
|
height: 100dvh;
|
|||
|
|
width: 100%;
|
|||
|
|
overflow-y: auto;
|
|||
|
|
overflow-x: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.content {
|
|||
|
|
flex: 1;
|
|||
|
|
padding: var(--space-xl);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Fullscreen workflow mode */
|
|||
|
|
.dashboard-layout.fullscreen-workflow {
|
|||
|
|
grid-template-columns: 1fr;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dashboard-layout.fullscreen-workflow .main-content {
|
|||
|
|
padding: 0;
|
|||
|
|
min-height: 100vh;
|
|||
|
|
min-height: 100dvh;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dashboard-layout.fullscreen-workflow .content {
|
|||
|
|
padding: 0;
|
|||
|
|
height: 100vh;
|
|||
|
|
height: 100dvh;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dashboard-layout.header-hidden .main-content {
|
|||
|
|
min-height: 100vh;
|
|||
|
|
min-height: 100dvh;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dashboard-layout.header-hidden .content {
|
|||
|
|
height: 100vh;
|
|||
|
|
height: 100dvh;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Responsive Design - 13 inch laptops */
|
|||
|
|
@media (max-width: 1366px) {
|
|||
|
|
.header-content {
|
|||
|
|
padding: var(--space-md) var(--space-lg);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.content {
|
|||
|
|
padding: var(--space-lg);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.banner-content {
|
|||
|
|
flex-direction: column;
|
|||
|
|
align-items: flex-start;
|
|||
|
|
gap: var(--space-sm);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.banner-actions {
|
|||
|
|
width: 100%;
|
|||
|
|
justify-content: flex-start;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.action-button {
|
|||
|
|
padding: var(--space-xs) var(--space-md);
|
|||
|
|
font-size: 0.8rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.plan-badge {
|
|||
|
|
font-size: 0.7rem;
|
|||
|
|
padding: 2px 6px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Responsive Design - Small laptops (1025px - 1280px) */
|
|||
|
|
@media (max-width: 1280px) and (min-width: 1025px) {
|
|||
|
|
/* Keep normal grid layout with sidebar in this range */
|
|||
|
|
.dashboard-layout {
|
|||
|
|
grid-template-columns: auto 1fr;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dashboard-layout.sidebar-collapsed {
|
|||
|
|
grid-template-columns: auto 1fr;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.header-content {
|
|||
|
|
padding: var(--space-sm) var(--space-md);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.content {
|
|||
|
|
padding: var(--space-md);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.right-section {
|
|||
|
|
gap: var(--space-md);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.trial-badge {
|
|||
|
|
font-size: 0.75rem;
|
|||
|
|
padding: 3px 10px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Tablet and below (overlay mode) */
|
|||
|
|
@media (max-width: 1024px) {
|
|||
|
|
.dashboard-layout {
|
|||
|
|
grid-template-columns: 1fr;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.sidebar-backdrop {
|
|||
|
|
display: block;
|
|||
|
|
position: fixed;
|
|||
|
|
top: 0;
|
|||
|
|
left: 0;
|
|||
|
|
right: 0;
|
|||
|
|
bottom: 0;
|
|||
|
|
background: rgba(0, 0, 0, 0.5);
|
|||
|
|
z-index: 999;
|
|||
|
|
backdrop-filter: blur(2px);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dashboard-layout.sidebar-collapsed .sidebar-backdrop {
|
|||
|
|
display: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.hamburger-menu {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.user-info {
|
|||
|
|
display: none !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.plan-display .plan-info {
|
|||
|
|
display: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.trial-info {
|
|||
|
|
margin-right: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.footer-content {
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: var(--space-md);
|
|||
|
|
text-align: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.footer-links {
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: var(--space-sm);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Mobile responsive */
|
|||
|
|
@media (max-width: 768px) {
|
|||
|
|
/* Breathing room above/below the 38px controls so they don't crowd the
|
|||
|
|
bottom border of the bar */
|
|||
|
|
.header-content {
|
|||
|
|
padding: 10px 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* The divider reads as a line touching the avatar at this size; the gap
|
|||
|
|
between the controls is enough separation on its own */
|
|||
|
|
.topbar-divider {
|
|||
|
|
display: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.user-menu {
|
|||
|
|
gap: 10px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.content {
|
|||
|
|
padding: var(--space-sm);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Bottom nav replaces the hamburger/drawer on phones */
|
|||
|
|
.hamburger-menu {
|
|||
|
|
display: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Reserve space for the fixed bottom nav */
|
|||
|
|
.dashboard-layout.has-bottom-nav .content {
|
|||
|
|
padding-bottom: calc(var(--bottom-nav-height) + var(--safe-bottom) + var(--space-sm));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dashboard-layout.has-bottom-nav .footer {
|
|||
|
|
display: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Full-height pages (Inbox): shrink the content area instead of padding it.
|
|||
|
|
flex: 0 0 auto is required — the default flex: 1 zeroes the flex basis and
|
|||
|
|
grows back to the full 100dvh, putting the page bottom under the nav. */
|
|||
|
|
.dashboard-layout.has-bottom-nav.header-hidden .content {
|
|||
|
|
flex: 0 0 auto;
|
|||
|
|
height: calc(100dvh - var(--bottom-nav-height) - var(--safe-bottom));
|
|||
|
|
padding-bottom: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.right-section {
|
|||
|
|
gap: var(--space-sm);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.banner-content {
|
|||
|
|
gap: var(--space-xs);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.banner-text {
|
|||
|
|
font-size: var(--text-sm);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.banner-actions {
|
|||
|
|
flex-direction: column;
|
|||
|
|
width: 100%;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.action-button {
|
|||
|
|
width: 100%;
|
|||
|
|
text-align: center;
|
|||
|
|
padding: var(--space-sm);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.trial-badge {
|
|||
|
|
font-size: 0.7rem;
|
|||
|
|
padding: 2px 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.notification-badge {
|
|||
|
|
min-width: 16px;
|
|||
|
|
height: 16px;
|
|||
|
|
font-size: 10px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.avatar {
|
|||
|
|
width: 28px;
|
|||
|
|
height: 28px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.avatar-wrapper {
|
|||
|
|
width: 28px;
|
|||
|
|
height: 28px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dropdown-menu {
|
|||
|
|
right: 0;
|
|||
|
|
left: auto;
|
|||
|
|
min-width: 140px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.footer {
|
|||
|
|
padding: var(--space-md);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.footer-content p {
|
|||
|
|
font-size: var(--text-sm);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Very small mobile devices */
|
|||
|
|
@media (max-width: 480px) {
|
|||
|
|
/* Keeps the 38px controls off the bar's bottom border */
|
|||
|
|
.header-content {
|
|||
|
|
padding: 9px 10px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.content {
|
|||
|
|
padding: var(--space-xs);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.plan-display {
|
|||
|
|
display: none !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.user-menu {
|
|||
|
|
gap: var(--space-xs);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.notification-button {
|
|||
|
|
padding: var(--space-xs);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.notification-icon {
|
|||
|
|
width: 20px;
|
|||
|
|
height: 20px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.message-limit-banner {
|
|||
|
|
padding: var(--space-sm);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.banner-content {
|
|||
|
|
gap: var(--space-xs);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.banner-text {
|
|||
|
|
font-size: var(--text-xs);
|
|||
|
|
flex-direction: column;
|
|||
|
|
align-items: flex-start;
|
|||
|
|
gap: var(--space-xs);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.banner-icon {
|
|||
|
|
font-size: 1em;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.action-button {
|
|||
|
|
font-size: 0.75rem;
|
|||
|
|
padding: var(--space-xs) var(--space-sm);
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.avatar {
|
|||
|
|
width: 24px;
|
|||
|
|
height: 24px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.avatar-wrapper {
|
|||
|
|
width: 24px;
|
|||
|
|
height: 24px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.status-indicator {
|
|||
|
|
width: 8px;
|
|||
|
|
height: 8px;
|
|||
|
|
border-width: 1px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dropdown-menu {
|
|||
|
|
min-width: 120px;
|
|||
|
|
font-size: var(--text-sm);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.menu-item {
|
|||
|
|
padding: var(--space-xs) var(--space-sm);
|
|||
|
|
font-size: var(--text-sm);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.footer {
|
|||
|
|
padding: var(--space-sm);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.footer-content {
|
|||
|
|
gap: var(--space-sm);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.footer-content p {
|
|||
|
|
font-size: var(--text-xs);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.footer-links a {
|
|||
|
|
font-size: var(--text-xs);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|