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:
Дмитрий
2026-05-12 14:32:03 +03:00
parent 4e27db63a3
commit 5c8ad2738a
6 changed files with 67 additions and 53 deletions
+32 -1
View File
@@ -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;
+2 -4
View File
@@ -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' },
]);
+6 -26
View File
@@ -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('Дашборд');
});