feat(layout): dark topbar + sidebar cleanup + DevIndexBadge moved below
Sidebar: убраны Менеджеры/Напоминания; Работа в порядке Проекты/Сделки/Канбан/Дашборд; Команда — только Настройки; снят useRemindersStore (был только под reminders badge). Topbar: тёмный фон linear-gradient(noir → #04261E) совпадающий с sidebar #1271; убран breadcrumb «Рабочая область»; v-toolbar__content padding-left:240 (не уходит под sidebar). DevIndexBadge: top:64 (ниже топбара, не перекрывает user-chip). Vitest AppLayout 15/15 PASS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+32
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "./dev-indices.schema.json",
|
||||
"version": 1,
|
||||
"lastId": 1379,
|
||||
"lastId": 1381,
|
||||
"entries": {
|
||||
"1": {
|
||||
"file": "resources/js/components/AppShell.vue",
|
||||
@@ -21673,6 +21673,37 @@
|
||||
"key": null,
|
||||
"ref": null,
|
||||
"createdAt": "2026-05-12T09:30:10.492Z"
|
||||
},
|
||||
"1380": {
|
||||
"file": "resources/js/components/layout/AppTopbar.vue",
|
||||
"line": 93,
|
||||
"tag": "span",
|
||||
"parentChain": [
|
||||
"AppTopbar",
|
||||
"v-app-bar",
|
||||
"div"
|
||||
],
|
||||
"signature": "resources/js/components/layout/AppTopbar::AppTopbar>v-app-bar>div::span[]::лидерра::0",
|
||||
"text": "Лидерра",
|
||||
"key": null,
|
||||
"ref": null,
|
||||
"createdAt": "2026-05-12T10:41:46.611Z"
|
||||
},
|
||||
"1381": {
|
||||
"file": "resources/js/components/layout/AppTopbar.vue",
|
||||
"line": 93,
|
||||
"tag": "span",
|
||||
"parentChain": [
|
||||
"AppTopbar",
|
||||
"v-app-bar",
|
||||
"div",
|
||||
"span"
|
||||
],
|
||||
"signature": "resources/js/components/layout/AppTopbar::AppTopbar>v-app-bar>div>span::span[]::.::0",
|
||||
"text": ".",
|
||||
"key": null,
|
||||
"ref": null,
|
||||
"createdAt": "2026-05-12T10:41:46.612Z"
|
||||
}
|
||||
},
|
||||
"deleted": {}
|
||||
|
||||
@@ -27,7 +27,7 @@ defineProps<{
|
||||
<style scoped>
|
||||
.dev-index-badge {
|
||||
position: fixed;
|
||||
top: 8px;
|
||||
top: 64px;
|
||||
right: 8px;
|
||||
z-index: 9000;
|
||||
display: inline-flex;
|
||||
|
||||
@@ -5,18 +5,16 @@
|
||||
* + active-marker pseudo-element + JetBrains Mono badges.
|
||||
*
|
||||
* Brand mark + nav-tree (3 группы: Работа, Финансы, Команда).
|
||||
* Counts для «Напоминания» — живой из remindersStore; «Сделки»/«Менеджеры» — mock.
|
||||
* Counts для «Сделки» — mock.
|
||||
*/
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useRemindersStore } from '../../stores/reminders';
|
||||
import Kbd from '../ui/Kbd.vue';
|
||||
|
||||
interface NavItem {
|
||||
title: string;
|
||||
icon: string;
|
||||
to: string;
|
||||
countKey?: 'deals' | 'reminders' | 'managers';
|
||||
count?: number;
|
||||
}
|
||||
interface NavGroup {
|
||||
@@ -27,23 +25,15 @@ interface NavGroup {
|
||||
const drawerOpen = defineModel<boolean>('drawerOpen', { default: true });
|
||||
|
||||
const route = useRoute();
|
||||
const reminders = useRemindersStore();
|
||||
|
||||
const navGroups = computed<NavGroup[]>(() => [
|
||||
{
|
||||
eyebrow: 'Работа',
|
||||
items: [
|
||||
{ title: 'Дашборд', icon: 'mdi-view-dashboard-outline', to: '/dashboard' },
|
||||
{ title: 'Проекты', icon: 'mdi-folder-multiple-outline', to: '/projects' },
|
||||
{ title: 'Сделки', icon: 'mdi-format-list-bulleted', to: '/deals', count: 247 },
|
||||
{ title: 'Канбан', icon: 'mdi-view-column-outline', to: '/kanban' },
|
||||
{ title: 'Проекты', icon: 'mdi-folder-multiple-outline', to: '/projects' },
|
||||
{
|
||||
title: 'Напоминания',
|
||||
icon: 'mdi-clock-outline',
|
||||
to: '/reminders',
|
||||
countKey: 'reminders',
|
||||
count: reminders.counts.active,
|
||||
},
|
||||
{ title: 'Дашборд', icon: 'mdi-view-dashboard-outline', to: '/dashboard' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -56,14 +46,12 @@ const navGroups = computed<NavGroup[]>(() => [
|
||||
{
|
||||
eyebrow: 'Команда',
|
||||
items: [
|
||||
{ title: 'Менеджеры', icon: 'mdi-account-group-outline', to: '/managers', count: 4 },
|
||||
{ title: 'Настройки', icon: 'mdi-cog-outline', to: '/settings' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
function resolveCount(item: NavItem): number {
|
||||
if (item.countKey === 'reminders') return reminders.counts.active;
|
||||
return item.count ?? 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -90,8 +90,6 @@ async function handleLogout(): Promise<void> {
|
||||
<v-app-bar-nav-icon class="d-md-none" @click="emit('toggle-drawer')" />
|
||||
|
||||
<div class="crumb">
|
||||
<span class="text-medium-emphasis">Рабочая область</span>
|
||||
<v-icon size="14" class="mx-1">mdi-chevron-right</v-icon>
|
||||
<strong>{{ pageTitle }}</strong>
|
||||
</div>
|
||||
|
||||
@@ -193,7 +191,16 @@ async function handleLogout(): Promise<void> {
|
||||
|
||||
<style scoped>
|
||||
.app-topbar {
|
||||
border-bottom: 1px solid #d9d5cd !important;
|
||||
background: linear-gradient(180deg, var(--liderra-noir) 0%, #04261E 100%) !important;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08) !important;
|
||||
color: #E8E2D4 !important;
|
||||
}
|
||||
.app-topbar :deep(.v-toolbar__content) {
|
||||
padding-left: 240px;
|
||||
color: #E8E2D4;
|
||||
}
|
||||
.app-topbar :deep(.v-icon) {
|
||||
color: #B8B0A0;
|
||||
}
|
||||
.crumb {
|
||||
display: flex;
|
||||
@@ -201,20 +208,30 @@ async function handleLogout(): Promise<void> {
|
||||
gap: 4px;
|
||||
font-size: 14px;
|
||||
margin-left: 8px;
|
||||
color: #E8E2D4;
|
||||
}
|
||||
.crumb strong {
|
||||
color: var(--liderra-ivory);
|
||||
font-weight: 600;
|
||||
}
|
||||
.searchbar {
|
||||
text-transform: none;
|
||||
color: #B8B0A0 !important;
|
||||
border-color: rgba(255, 255, 255, 0.12) !important;
|
||||
}
|
||||
.search-kbd {
|
||||
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
||||
font-size: 10px;
|
||||
padding: 1px 5px;
|
||||
border: 1px solid #d9d5cd;
|
||||
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||
border-radius: 3px;
|
||||
background: #f0ede4;
|
||||
color: #66635c;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: #9B9484;
|
||||
margin-left: 6px;
|
||||
}
|
||||
.user-chip :deep(.v-btn__content) {
|
||||
color: #E8E2D4;
|
||||
}
|
||||
.notification-pip {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
|
||||
@@ -29,14 +29,12 @@ const drawerOpen = ref(true);
|
||||
// Тот же навигационный pool что в AppSidebar — для crumb-resolution в topbar
|
||||
// (sidebar и topbar — независимые, но navGroups совпадают по контракту).
|
||||
const navItems = computed(() => [
|
||||
{ title: 'Дашборд', to: '/dashboard' },
|
||||
{ title: 'Проекты', to: '/projects' },
|
||||
{ title: 'Сделки', to: '/deals' },
|
||||
{ title: 'Канбан', to: '/kanban' },
|
||||
{ title: 'Проекты', to: '/projects' },
|
||||
{ title: 'Напоминания', to: '/reminders' },
|
||||
{ title: 'Дашборд', to: '/dashboard' },
|
||||
{ title: 'Биллинг', to: '/billing' },
|
||||
{ title: 'Отчёты', to: '/reports' },
|
||||
{ title: 'Менеджеры', to: '/managers' },
|
||||
{ title: 'Настройки', to: '/settings' },
|
||||
]);
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ const mockUser: AuthUser = {
|
||||
last_login_at: null,
|
||||
};
|
||||
|
||||
// AppLayout содержит sidebar (8 nav-items в 3 группах) + topbar (crumb/search/user) + RouterView.
|
||||
// AppLayout содержит sidebar (6 nav-items в 3 группах) + topbar (crumb/search/user) + RouterView.
|
||||
|
||||
const mountAppLayout = async (path = '/dashboard', user: AuthUser | null = mockUser) => {
|
||||
setActivePinia(createPinia());
|
||||
@@ -52,10 +52,9 @@ const mountAppLayout = async (path = '/dashboard', user: AuthUser | null = mockU
|
||||
{ path: '/dashboard', component: { template: '<div>dashboard</div>' } },
|
||||
{ path: '/deals', component: { template: '<div>deals</div>' } },
|
||||
{ path: '/kanban', component: { template: '<div>kanban</div>' } },
|
||||
{ path: '/reminders', component: { template: '<div>reminders</div>' } },
|
||||
{ path: '/projects', component: { template: '<div>projects</div>' } },
|
||||
{ path: '/billing', component: { template: '<div>billing</div>' } },
|
||||
{ path: '/reports', component: { template: '<div>reports</div>' } },
|
||||
{ path: '/managers', component: { template: '<div>managers</div>' } },
|
||||
{ path: '/settings', component: { template: '<div>settings</div>' } },
|
||||
],
|
||||
});
|
||||
@@ -85,43 +84,24 @@ describe('AppLayout.vue', () => {
|
||||
expect(text).toContain('Команда');
|
||||
});
|
||||
|
||||
it('содержит все 8 nav-пунктов', async () => {
|
||||
it('содержит все 6 nav-пунктов (Менеджеры+Напоминания убраны по требованию заказчика)', async () => {
|
||||
const wrapper = await mountAppLayout();
|
||||
const text = wrapper.text();
|
||||
['Дашборд', 'Сделки', 'Канбан', 'Напоминания', 'Биллинг', 'Отчёты', 'Менеджеры', 'Настройки'].forEach((label) =>
|
||||
['Проекты', 'Сделки', 'Канбан', 'Дашборд', 'Биллинг', 'Отчёты', 'Настройки'].forEach((label) =>
|
||||
expect(text).toContain(label),
|
||||
);
|
||||
expect(text).not.toContain('Менеджеры');
|
||||
expect(text).not.toContain('Напоминания');
|
||||
});
|
||||
|
||||
it('показывает счётчики только у пунктов с count', async () => {
|
||||
const wrapper = await mountAppLayout();
|
||||
const text = wrapper.text();
|
||||
expect(text).toContain('247'); // Сделки (mock)
|
||||
expect(text).toContain('4'); // Менеджеры (mock)
|
||||
// Напоминания: counts.active=0 → бейдж скрыт по новому правилу (count > 0).
|
||||
});
|
||||
|
||||
it('бейдж напоминаний скрыт при counts.active=0', async () => {
|
||||
const wrapper = await mountAppLayout();
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.find('[data-testid="nav-count-reminders"]').exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('бейдж напоминаний показывается при counts.active>0', async () => {
|
||||
const { useRemindersStore } = await import('../../resources/js/stores/reminders');
|
||||
const wrapper = await mountAppLayout();
|
||||
const reminders = useRemindersStore();
|
||||
reminders.counts.active = 7;
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const badge = wrapper.find('[data-testid="nav-count-reminders"]');
|
||||
expect(badge.exists()).toBe(true);
|
||||
expect(badge.text()).toBe('7');
|
||||
});
|
||||
|
||||
it('breadcrumb показывает текущую страницу', async () => {
|
||||
const wrapper = await mountAppLayout('/dashboard');
|
||||
expect(wrapper.text()).toContain('Рабочая область');
|
||||
expect(wrapper.text()).toContain('Дашборд');
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user