diff --git a/cspell-words.txt b/cspell-words.txt
index 2d4babd5..97df4031 100644
--- a/cspell-words.txt
+++ b/cspell-words.txt
@@ -1436,3 +1436,6 @@ evals
волатильны
незакоммиченное
бандл
+
+# План «Сделки drawer + редактирование источника» (2026-05-18)
+табах
diff --git a/docs/superpowers/plans/2026-05-18-deals-drawer-and-project-source-edit.md b/docs/superpowers/plans/2026-05-18-deals-drawer-and-project-source-edit.md
new file mode 100644
index 00000000..f8c6a7d4
--- /dev/null
+++ b/docs/superpowers/plans/2026-05-18-deals-drawer-and-project-source-edit.md
@@ -0,0 +1,885 @@
+# Deals drawer + project source edit — Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Привести drawer-«легенду» сделки и карточку проекта к запросу заказчика 18.05.2026 — статус-picker, корректные параметры (Тип/Источник), selected-driven видимость drawer/bulk-полосы, редактирование источника проекта.
+
+**Architecture:** 5 атомарных задач (1 коммит = 1 task). Frontend-only задачи 1-4 (Vue/TS). Задача 5 расширяет backend (UpdateProjectRequest + ProjectController) + UI ProjectDetailsDrawer. TDD per task: failing test → minimal impl → vitest/pest → commit.
+
+**Tech Stack:** Vue 3 + Vuetify 3 + Pinia, Laravel 13 + Pest 4, axios + ApiClient.
+
+**Источник истины** для решений: AskUserQuestion ответы 18.05.2026:
+
+- п.1: «при выборе 1 сделки она не нужна, нужна только легенда справа»
+- п.2: «при выборе 2-х и более легенда не нужна а полоса нужна»
+- п.3: статус в drawer кликабельный, dropdown статусов
+- п.4: убрать «Менеджер»/«Не назначен»
+- п.5: B-префикс уже убран (commit `36ea9cd`)
+- п.6: формат «отправитель + (ключевое слово как в карточке создания)» = `signal_identifier` для site/call; для sms — `sms_senders[0]` + `(${sms_keyword})` если есть
+- п.7: «Тип» (Сайт/Звонок/СМС) вместо «Менеджер»
+- п.8: подпись «Источник» над полями на 3 табах NewProjectDialog
+- п.9: редактировать источник **только в карточке проекта** (ProjectDetailsDrawer на /projects); в drawer сделки источник read-only
+
+---
+
+## File Structure
+
+| Файл | Что делает |
+|---|---|
+| `app/resources/js/views/DealsView.vue` | Selected-driven: drawer hidden при ≥2 selected; auto-open при selected=1 |
+| `app/resources/js/components/deals/DealDetailHero.vue` | StatusPill → inline statuspicker (`v-menu` со списком статусов) |
+| `app/resources/js/components/deals/DealDetailBody.vue` | Убрать «Менеджер», добавить «Тип» + «Источник» (read-only) |
+| `app/resources/js/composables/mockDeals.ts` | +поля projectSignalType / projectSignalIdentifier / projectSmsSenders / projectSmsKeyword |
+| `app/resources/js/composables/dealsApiMapper.ts` | Маппинг новых API-полей |
+| `app/resources/js/api/deals.ts` | Расширить ApiDeal интерфейс новыми полями |
+| `app/app/Http/Controllers/Api/DealController.php` | Eager-load + отдавать новые поля проекта в payload |
+| `app/resources/js/views/projects/NewProjectDialog.vue` | Подпись «Источник» над полями на 3 табах |
+| `app/resources/js/components/projects/ProjectDetailsDrawer.vue` | Добавить редактирование signal_identifier (site/call) + sms_senders/keyword (sms) |
+| `app/app/Http/Requests/UpdateProjectRequest.php` | +правила валидации signal_identifier по signal_type проекта |
+| `app/app/Http/Controllers/Api/ProjectController.php` | update() — пропустить signal_identifier в Project::update |
+| `app/resources/js/stores/projectsStore.ts` | Project type — поле signal_identifier ОК; проверить только |
+
+---
+
+## Task 1: Selected-driven drawer visibility (пп. 1+2)
+
+**Files:**
+
+- Modify: `app/resources/js/views/DealsView.vue` (полная логика panelOpen ↔ selected.length)
+- Test: `app/tests/Frontend/DealsView.spec.ts` (расширить существующий)
+
+**Логика:**
+
+- `selected.length === 0` → row-click открывает drawer (как сейчас)
+- `selected.length === 1` → drawer **авто-открыт для этой сделки**, bulk-полоса **скрыта**
+- `selected.length >= 2` → drawer **закрыт**, bulk-полоса видна
+
+DealsBulkBar уже показывается только при `selectedCount > 0` (нужно перепроверить — возможно показать при `>= 2` only).
+
+- [ ] **Step 1: Failing test для авто-открытия при selected=1**
+
+В `app/tests/Frontend/DealsView.spec.ts` добавить:
+
+```ts
+it('при selected=1 drawer авто-открывается на выбранной сделке, bulk-полоса скрыта', async () => {
+ const w = mount(DealsView, { global: { plugins: [vuetify, createPinia()] } });
+ await flushPromises();
+ w.vm.dealsState.push({ id: 42, name: 'X', phone: '+79991234567', statusSlug: 'new', project: 'p', manager: { initials: 'A', name: 'A' }, cost: 0, receivedMinutesAgo: 0 } as never);
+ w.vm.selected = [42];
+ await nextTick();
+ expect(w.vm.panelOpen).toBe(true);
+ expect(w.vm.selectedDeal?.id).toBe(42);
+});
+
+it('при selected>=2 drawer закрывается', async () => {
+ const w = mount(DealsView, { global: { plugins: [vuetify, createPinia()] } });
+ await flushPromises();
+ w.vm.dealsState.push({ id: 42, name: 'X', phone: '+1', statusSlug: 'new', project: 'p', manager: { initials: 'A', name: 'A' }, cost: 0, receivedMinutesAgo: 0 } as never);
+ w.vm.dealsState.push({ id: 43, name: 'Y', phone: '+2', statusSlug: 'new', project: 'p', manager: { initials: 'A', name: 'A' }, cost: 0, receivedMinutesAgo: 0 } as never);
+ w.vm.panelOpen = true;
+ w.vm.selectedDeal = w.vm.dealsState[0];
+ w.vm.selected = [42, 43];
+ await nextTick();
+ expect(w.vm.panelOpen).toBe(false);
+});
+```
+
+- [ ] **Step 2: Запустить тест — должен FAIL**
+
+`cd app && npx vitest run tests/Frontend/DealsView.spec.ts -t "при selected" --reporter=verbose`
+
+- [ ] **Step 3: Добавить watcher в DealsView.vue**
+
+Найти `watch([filterStatus, filterProject, receivedFrom, receivedTo, perPage], …)` (~строка 108) и **после** добавить:
+
+```ts
+// Selected-driven drawer visibility (18.05.2026 ux-request):
+// 0 selected → drawer по row-click; 1 selected → авто-открыт для этой сделки;
+// ≥2 selected → закрыт (показывается bulk-полоса).
+watch(selected, (ids) => {
+ if (ids.length === 1) {
+ const deal = dealsState.find((d) => d.id === ids[0]);
+ if (deal) {
+ selectedDeal.value = deal;
+ panelOpen.value = true;
+ }
+ } else if (ids.length >= 2) {
+ panelOpen.value = false;
+ }
+});
+```
+
+И **скрыть bulk-полосу при selected=1** — изменить отображение DealsBulkBar:
+
+```vue
+
+```
+
+- [ ] **Step 4: Vitest пройти GREEN**
+
+`cd app && npx vitest run tests/Frontend/DealsView.spec.ts --reporter=default`
+Expected: все passes, в т.ч. 2 новых.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add app/resources/js/views/DealsView.vue app/tests/Frontend/DealsView.spec.ts
+git commit -m "feat(deals): drawer виден при selected≤1, bulk-полоса только при ≥2"
+```
+
+---
+
+## Task 2: API: project source fields в drawer сделки (пп. 4+6+7)
+
+**Files:**
+
+- Modify: `app/app/Http/Controllers/Api/DealController.php` (eager-load + payload)
+- Modify: `app/resources/js/api/deals.ts` (ApiDeal +4 поля)
+- Modify: `app/resources/js/composables/mockDeals.ts` (MockDeal +4 поля)
+- Modify: `app/resources/js/composables/dealsApiMapper.ts` (маппер +4 поля)
+- Modify: `app/resources/js/components/deals/DealDetailBody.vue` (UI: убрать Менеджер, +Тип, +Источник)
+- Test: `app/tests/Feature/Deals/DealShowEndpointTest.php` или подобный для controller; `app/tests/Frontend/DealDetailBody.spec.ts` если есть, иначе расширить DealDetailDrawer.spec.ts
+
+- [ ] **Step 1: Pest failing для API payload**
+
+В существующем тесте `app/tests/Feature/Deals/*.php` для GET /api/deals/{id} добавить assertion:
+
+```php
+it('returns project signal_identifier/sms_keyword/sms_senders in deal payload', function () {
+ $tenant = Tenant::factory()->create();
+ $project = Project::factory()->create([
+ 'tenant_id' => $tenant->id,
+ 'signal_type' => 'sms',
+ 'signal_identifier' => 'MTS',
+ 'sms_senders' => ['MTS', 'BEELINE'],
+ 'sms_keyword' => 'КРЕДИТ',
+ ]);
+ $deal = Deal::factory()->create(['tenant_id' => $tenant->id, 'project_id' => $project->id]);
+
+ actingAsTenant($tenant);
+ $resp = $this->getJson("/api/deals/{$deal->id}?tenant_id={$tenant->id}");
+
+ $resp->assertOk()->assertJsonPath('deal.project_signal_identifier', 'MTS');
+ $resp->assertJsonPath('deal.project_sms_keyword', 'КРЕДИТ');
+ $resp->assertJsonPath('deal.project_sms_senders.0', 'MTS');
+});
+```
+
+- [ ] **Step 2: Запустить — FAIL**
+
+```
+cd app && ./vendor/bin/pest --filter "returns project signal_identifier"
+```
+
+Expected: FAIL (поля отсутствуют в payload).
+
+- [ ] **Step 3: Расширить DealController eager-load + transformer**
+
+В `app/app/Http/Controllers/Api/DealController.php`:
+
+- Строка 112 (`->with(['project:id,name,signal_type', ...])`) → расширить:
+ `->with(['project:id,name,signal_type,signal_identifier,sms_keyword,sms_senders', 'manager:id,email,first_name,last_name']);`
+- Строка 215 (`'project_signal_type' => …`) → добавить ниже:
+
+```php
+'project_signal_identifier' => $d->project?->signal_identifier,
+'project_sms_keyword' => $d->project?->sms_keyword,
+'project_sms_senders' => $d->project?->sms_senders,
+```
+
+Найти аналогичные места в `show()` методе (если есть) и добавить там же.
+
+- [ ] **Step 4: Pest passes**
+
+```
+cd app && ./vendor/bin/pest --filter "returns project signal_identifier" -v
+```
+
+Expected: PASS.
+
+- [ ] **Step 5: Расширить TypeScript интерфейсы**
+
+В `app/resources/js/api/deals.ts` интерфейс `ApiDeal` (строка 153) — добавить:
+
+```ts
+project_signal_identifier: string | null;
+project_sms_keyword: string | null;
+project_sms_senders: string[] | null;
+```
+
+В `app/resources/js/composables/mockDeals.ts` интерфейс `MockDeal` (строка 10) — добавить:
+
+```ts
+projectSignalType?: 'site' | 'call' | 'sms' | null;
+projectSignalIdentifier?: string | null;
+projectSmsKeyword?: string | null;
+projectSmsSenders?: string[] | null;
+```
+
+В `app/resources/js/composables/dealsApiMapper.ts` функция `mapApiDeal` — добавить маппинг новых полей.
+**ПРИМЕЧАНИЕ:** прочитать актуальный файл перед правкой, см. поля `signalType: d.project_signal_type as MockDeal['signalType']` — добавить аналогично:
+
+```ts
+projectSignalType: d.project_signal_type as MockDeal['projectSignalType'],
+projectSignalIdentifier: d.project_signal_identifier,
+projectSmsKeyword: d.project_sms_keyword,
+projectSmsSenders: d.project_sms_senders,
+```
+
+- [ ] **Step 6: Failing Vitest test для UI Drawer**
+
+Создать `app/tests/Frontend/DealDetailBody.spec.ts` (или расширить, если есть):
+
+```ts
+import { describe, it, expect } from 'vitest';
+import { mount } from '@vue/test-utils';
+import { createVuetify } from 'vuetify';
+import { setActivePinia, createPinia } from 'pinia';
+import DealDetailBody from '../../resources/js/components/deals/DealDetailBody.vue';
+import type { MockDeal } from '../../resources/js/composables/mockDeals';
+
+const vuetify = createVuetify();
+setActivePinia(createPinia());
+
+function makeDeal(overrides: Partial = {}): MockDeal {
+ return {
+ id: 1, name: 'A', phone: '+79991234567', statusSlug: 'new',
+ project: 'p', manager: { initials: 'AD', name: 'A' }, cost: 0,
+ receivedMinutesAgo: 1,
+ projectSignalType: 'site', projectSignalIdentifier: 'krk-finance.ru',
+ projectSmsKeyword: null, projectSmsSenders: null,
+ ...overrides,
+ };
+}
+
+describe('DealDetailBody — Тип и Источник (18.05.2026)', () => {
+ it('показывает Тип «Сайт» и Источник = signal_identifier для site', () => {
+ const w = mount(DealDetailBody, {
+ props: { deal: makeDeal() },
+ global: { plugins: [vuetify, createPinia()] },
+ });
+ expect(w.text()).toContain('Сайт');
+ expect(w.text()).toContain('krk-finance.ru');
+ });
+
+ it('для sms показывает sender + (keyword)', () => {
+ const w = mount(DealDetailBody, {
+ props: { deal: makeDeal({
+ projectSignalType: 'sms',
+ projectSignalIdentifier: null,
+ projectSmsSenders: ['MTS', 'BEELINE'],
+ projectSmsKeyword: 'КРЕДИТ',
+ }) },
+ global: { plugins: [vuetify, createPinia()] },
+ });
+ expect(w.text()).toContain('СМС');
+ expect(w.text()).toContain('MTS (КРЕДИТ)');
+ });
+
+ it('для sms без keyword показывает только sender', () => {
+ const w = mount(DealDetailBody, {
+ props: { deal: makeDeal({
+ projectSignalType: 'sms',
+ projectSignalIdentifier: null,
+ projectSmsSenders: ['MTS'],
+ projectSmsKeyword: null,
+ }) },
+ global: { plugins: [vuetify, createPinia()] },
+ });
+ expect(w.text()).toContain('СМС');
+ expect(w.text()).toContain('MTS');
+ expect(w.text()).not.toMatch(/\([^)]*\)/);
+ });
+
+ it('не отображает «Менеджер» секцию', () => {
+ const w = mount(DealDetailBody, {
+ props: { deal: makeDeal() },
+ global: { plugins: [vuetify, createPinia()] },
+ });
+ expect(w.text()).not.toContain('Менеджер');
+ });
+});
+```
+
+- [ ] **Step 7: Run — FAIL**
+
+`cd app && npx vitest run tests/Frontend/DealDetailBody.spec.ts --reporter=verbose`
+
+Expected: 4 fails.
+
+- [ ] **Step 8: Реализация DealDetailBody.vue**
+
+В `app/resources/js/components/deals/DealDetailBody.vue`:
+
+1. Добавить helpers в `