# Quiet Luxury редизайн элементов #1440 и #896 — design spec

**Дата:** 2026-05-12
**Скоуп:** буквально два элемента из Dev Element Indices.
**Цель:** привести два визуально «тяжёлых» / выпадающих из Quiet Luxury (Portal Redesign I1) элемента к языку токенов `tokens.css`.
**Связанные документы:** [Portal Redesign — Quiet Luxury (I1)](2026-05-12-portal-redesign-quiet-luxury-design.md), [Dev Element Indices](2026-05-12-dev-element-indices-design.md).
**Status:** approved sections (1–5), pending user review of written spec.

---

## 1. Контекст и проблема

### 1.1. Элемент #1440 — `span.card-check__box` в `ProjectCard.vue`

- Pin: [app/resources/js/components/projects/ProjectCard.vue:11](../../app/resources/js/components/projects/ProjectCard.vue#L11).
- Сейчас: `20×20px`, бордер `2px solid var(--liderra-noir)` (= `#012019`), checked-стейт — полная заливка `var(--liderra-teal)` (`#0F6E56`) + белая `::after` галочка.
- Проблема: на ivory-карточке тёмная толстая `2px noir` обводка читается как «штамп». В сетке из 10+ карточек чек-боксы доминируют визуально и противоречат языку `tokens.css`, где базовая сетка границ — `--liderra-line` (`rgba(1, 32, 25, 0.08)`, 1px) и `--liderra-line-strong` (`rgba(1, 32, 25, 0.14)`, 1px).

### 1.2. Элемент #896 — `v-text-field` «Название проекта» в `NewProjectDialog.vue`

- Pin: [app/resources/js/views/projects/NewProjectDialog.vue:59](../../app/resources/js/views/projects/NewProjectDialog.vue#L59).
- Сейчас: `<v-text-field>` использует global Vuetify-defaults из [app/resources/js/plugins/vuetify.ts:50-54](../../app/resources/js/plugins/vuetify.ts#L50-L54) — `variant=outlined density=comfortable color=primary`. Outline-border берёт дефолтный Vuetify-цвет `rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity))` = ≈ `rgba(1, 32, 25, 0.6)` — заметно темнее, чем `--liderra-line` / `--liderra-line-strong`.
- Проблема: тёмный outline-border выпадает из Quiet Luxury языка (1px @ 6–8–14 % noir в `tokens.css`). На ivory-диалоге все v-text-field читаются как «офисные», а не «премиум». То же касается v-combobox и v-autocomplete (они тоже используют `.v-field__outline__*` под капотом), но они out-of-scope этой задачи и должны быть переделаны отдельным PR.
- **Post-discovery rescope (2026-05-12):** при Task 0 имплементации обнаружено global default `variant=outlined`, поэтому первоначальная гипотеза «#896 — единственное filled поле, выпадающее среди других» оказалась неверна. Заказчик расширил область до **всех 5 `<v-text-field>`** в `NewProjectDialog.vue` (lines 21, 30, 48, 59, 61). См. §3.2 для актуального списка правок.

### 1.3. Известные близнецы (вне области по решению заказчика)

- `.toolbar-check__box` в [app/resources/js/views/ProjectsView.vue:50](../../app/resources/js/views/ProjectsView.vue#L50) — идентичные CSS-правила. Останется в текущем виде; следствие: после применения этой спеки на странице будет два разных стиля checkbox (карточка ≠ toolbar). Это **осознанный trade-off**.
- `v-combobox` (sms_senders, line 40), `v-autocomplete` (regions, line 70), `v-btn-toggle` (days, line 83) в `NewProjectDialog.vue` — останутся с дефолтным Vuetify-стилем. Они используют те же `.v-field__outline__*` (combobox / autocomplete) и должны быть переделаны отдельной задачей; в текущей области НЕ затрагиваются.

---

## 2. Архитектура

Узкая CSS / prop-правка, **без новых компонентов и primitives**:

- **`ProjectCard.vue`** — переписать `<style scoped>` (текущие строки 132–172). Template / script нетронуты **в рамках этой задачи**. Имена классов сохранены (`.card-check`, `.card-check__box`) → существующие Vitest-тесты с `findByTestId('card-select')` не ломаются, dev-indices signature для #1440 (line 11) не сдвигается.

  **Note (post-impl audit `3fc90f1`):** на момент начала сессии working dir уже содержал uncommitted template change `<v-checkbox>` → `<label><input type="checkbox"><span class="card-check__box" /></label>` (см. session-start `git status: M app/resources/js/components/projects/ProjectCard.vue`). Branch-HEAD `88a13e2` имел `<v-checkbox>`. Сессия работала поверх pre-modified working dir, и impl-коммит `3fc90f1` забрал этот template change вместе с CSS. Архитектурно изменение **необходимо** для работы Quiet Luxury CSS — `<v-checkbox>` рендерит Vuetify-internal `<input>` без sibling-`<span>`, который требуется scoped `:checked + .card-check__box::after`-селектору. Тестирование (Vitest 4/4 + baseline-fix commit `84530d5` подготовлен под native input selector) подтверждает корректность.
- **`NewProjectDialog.vue`** — на line 59 расширить props у `<v-text-field>`; добавить `<style scoped>` блок в конец файла (сейчас файл без scoped-стилей). Template/script — без структурных изменений → dev-indices signature для #896 не сдвигается.

Никаких изменений в `tokens.css`, `app.css`, `motion.css`, `typography.css`. Используем только существующие токены и Vuetify 3 API.

---

## 3. Точная реализация

### 3.1. #1440 — новый `<style scoped>` в `ProjectCard.vue`

Полная замена текущего блока (current lines 132–172):

```css
<style scoped>
.project-card.paused {
    opacity: 0.75;
}
.card-check {
    display: inline-flex;
    align-items: center;
    cursor: pointer;
    padding: 4px;
}
.card-check input {
    position: absolute;
    opacity: 0;
    pointer-events: none;
}
.card-check__box {
    width: 16px;
    height: 16px;
    border: 1px solid var(--liderra-line);
    border-radius: var(--radius-6);
    background: var(--liderra-surface);
    display: inline-block;
    position: relative;
    transition:
        border-color 200ms cubic-bezier(0.16, 1, 0.3, 1),
        background-color 200ms cubic-bezier(0.16, 1, 0.3, 1);
}
.card-check:hover .card-check__box {
    border-color: var(--liderra-line-strong);
}
.card-check input:focus-visible + .card-check__box {
    outline: 2px solid var(--liderra-teal);
    outline-offset: 2px;
}
.card-check input:checked + .card-check__box {
    background: rgba(15, 110, 86, 0.10);
    border-color: var(--liderra-teal);
}
.card-check input:checked + .card-check__box::after {
    content: '';
    position: absolute;
    left: 4px;
    top: 0;
    width: 5px;
    height: 9px;
    border: solid var(--liderra-teal);
    border-width: 0 1.5px 1.5px 0;
    transform: rotate(45deg);
}
</style>
```

Delta vs. текущий код:

- Размер `20→16px` (соответствует масштабу FilterChip / Kbd UI primitives).
- Бордер `2px solid noir → 1px solid --liderra-line` (idle) + `--liderra-line-strong` (hover).
- Checked-стейт `полная teal-заливка + белая stroke → tonal 10% teal-bg + teal-stroke` (как FilterChip-active в [app/resources/js/components/ui/FilterChip.vue:42-46](../../app/resources/js/components/ui/FilterChip.vue#L42-L46)).
- Толщина stroke галочки `2px → 1.5px` (пропорция).
- Координаты галочки пересчитаны под 16×16: `left: 4px; top: 0; width: 5px; height: 9px`.
- Добавлен `:focus-visible` outline (раньше отсутствовал — keyboard-навигация невидима).
- Transition расширен на `background-color` (для плавного входа в checked).

### 3.2. #896 + 4 близнеца — class на 5 v-text-field + новый `<style scoped>` в `NewProjectDialog.vue`

**Template edits** — добавить `class="ld-input-quiet"` к **каждому из 5** v-text-field в файле. Остальные props (variant/density/color) **НЕ нужны** — они уже задаются global default'ом в [vuetify.ts:50-54](../../app/resources/js/plugins/vuetify.ts#L50-L54). Точные правки:

| Line | Поле | Текущий | Целевой |
|---|---|---|---|
| 21 | Домен (site) | `<v-text-field v-model="form.signal_identifier" label="..." placeholder="..." :readonly="..." :error-messages="..." />` | `+ class="ld-input-quiet"` |
| 30 | Номер (call) | `<v-text-field v-model="form.signal_identifier" label="..." placeholder="..." hint="..." :readonly="..." :error-messages="..." />` | `+ class="ld-input-quiet"` |
| 48 | Ключевое слово (sms) | `<v-text-field v-model="form.sms_keyword" label="..." hint="..." :error-messages="..." />` | `+ class="ld-input-quiet"` |
| 59 | Название проекта | `<v-text-field v-model="form.name" label="Название проекта" :error-messages="errors.name" />` | `+ class="ld-input-quiet"` |
| 61 | Лимит лидов в день | `<v-text-field v-model.number="form.daily_limit_target" label="..." type="number" min="1" max="10000" :error-messages="..." />` | `+ class="ld-input-quiet"` |

Добавляется только один новый атрибут `class="ld-input-quiet"` — все существующие props сохраняются (v-model, label, placeholder, hint, readonly, type, min/max, error-messages).

**Новый `<style scoped>` блок** в конце файла (после `</script>` line 208):

```vue
<style scoped>
.ld-input-quiet :deep(.v-field) {
    border-radius: var(--radius-8);
}
.ld-input-quiet :deep(.v-field__outline__start),
.ld-input-quiet :deep(.v-field__outline__end),
.ld-input-quiet :deep(.v-field__outline__notch::before),
.ld-input-quiet :deep(.v-field__outline__notch::after) {
    border-color: var(--liderra-line);
    opacity: 1;
    transition: border-color 200ms cubic-bezier(0.16, 1, 0.3, 1);
}
.ld-input-quiet :deep(.v-field:hover .v-field__outline__start),
.ld-input-quiet :deep(.v-field:hover .v-field__outline__end),
.ld-input-quiet :deep(.v-field:hover .v-field__outline__notch::before),
.ld-input-quiet :deep(.v-field:hover .v-field__outline__notch::after) {
    border-color: var(--liderra-line-strong);
    opacity: 1;
}
.ld-input-quiet :deep(.v-field--focused .v-field__outline__start),
.ld-input-quiet :deep(.v-field--focused .v-field__outline__end),
.ld-input-quiet :deep(.v-field--focused .v-field__outline__notch::before),
.ld-input-quiet :deep(.v-field--focused .v-field__outline__notch::after) {
    border-color: var(--liderra-teal);
    opacity: 1;
}
.ld-input-quiet :deep(.v-field--error:not(.v-field--disabled) .v-field__outline__start),
.ld-input-quiet :deep(.v-field--error:not(.v-field--disabled) .v-field__outline__end),
.ld-input-quiet :deep(.v-field--error:not(.v-field--disabled) .v-field__outline__notch::before),
.ld-input-quiet :deep(.v-field--error:not(.v-field--disabled) .v-field__outline__notch::after) {
    border-color: currentColor;
    opacity: 1;
}
</style>
```

Обоснования:

- `class="ld-input-quiet"` — уникальный класс, не зацепляет v-combobox / v-autocomplete (они остаются с дефолтным border-цветом темы, см. §1.3).
- `:deep(.v-field) { border-radius: var(--radius-8); }` — radius из tokens.css (8px). Заменяет первоначальный `rounded="lg"` prop — надёжнее (не зависит от Vuetify internal mapping) и работает на все 5 полей через один CSS rule.
- `:deep()` нужен потому что Vuetify-classes находятся в shadow-children компонента (Vue scoped-стили не пробивают границу без `:deep()`).
- **`opacity: 1` обязателен** на каждом override-блоке (Critical-1 от reviewer'а Task 2): Vuetify outlined использует `opacity: var(--v-field-border-opacity)` = `0.38` на `.v-field__outline` для idle-стейта. Без явного `opacity: 1` наш `--liderra-line` (alpha 0.08) даёт effective alpha 0.03 → border невидим.
- **Hover селектор `.v-field:hover` внутри `:deep()` (НЕ `.ld-input-quiet:hover`)** (Important-3 от reviewer'а): root класс находится на `.v-input` wrapper, который включает hint/error-message-область под полем. Vuetify own hover targets `.v-field:hover` (inner box). Перенесли scope чтобы hover не триггерился при наведении на сообщение об ошибке.
- **Error-стейт rule** (Critical-2 от reviewer'а): Vuetify применяет `color: rgb(var(--v-theme-error))` к `.v-field--error.v-field__outline`, а sub-элементы наследуют через `border: 0 solid currentColor`. Наш override `border-color` явным значением разрывает `currentColor`-наследование → red error border не появляется. Восстанавливаем через explicit `border-color: currentColor; opacity: 1` для `.v-field--error:not(.v-field--disabled)`.
- Focused-стейт label / caret подсветится `var(--liderra-teal)` через `color="primary"` из global default.
- `error-messages` биндинг (Vuetify-native) сохранён — error-стейт работает штатно.

---

## 4. Тестирование и верификация

### 4.1. Vitest

- Существующие spec'и для `ProjectCard.vue` и `NewProjectDialog.vue` (если есть в репозитории на ветке `plan5-frontend-projects` — проверить во время implementation; список тестов pin'нуть в plan) — должны проходить без правок, потому что:
  - Сохраняется атрибут `data-testid="card-select"` на `.card-check` ([ProjectCard.vue:5](../../app/resources/js/components/projects/ProjectCard.vue#L5)) и checkbox-input с `@change="$emit('toggle-select', project.id)"`.
  - Сохраняется `v-model="form.name"` и `:error-messages="errors.name"` на v-text-field — submit-флоу и валидация не задеты.
- Не верифицировал на момент написания спеки: точное название и присутствие spec-файлов для этих двух компонентов. Plan уточнит.

### 4.2. Histoire

- Если есть `ProjectCard.story.vue` / `NewProjectDialog.story.vue` — visual smoke через `npm run story` (Histoire dev-сервер), проверить state'ы `idle / hover / focus-visible / checked / partial / error`. Если story файлов нет — не блокер, но добавлять их в этой задаче **не нужно** (out-of-scope).

### 4.3. A11y

- **Контраст border idle (#1440):** `--liderra-line` (`rgba(1, 32, 25, 0.08)`) на `#FFFFFF` → ~1.15:1. WCAG SC 1.4.11 (non-text contrast 3:1) применяется к UI-компонентам в активном состоянии и focus indicator'ам; idle border decorative-checkbox формально не входит в strict requirement, но граничный случай — фиксируем в trade-off (вариант B обеспечивал 1.14:1 тоже, выбор пользователем сделан осознанно).
- **Контраст focus-visible outline:** `--liderra-teal` (`#0F6E56`) на `#FFFFFF` → 4.7:1, проходит SC 1.4.11.
- **Контраст checked tonal-bg:** `rgba(15, 110, 86, 0.10)` поверх `#FFF` → effective `~#E7F1ED`. Галочка teal на этом фоне: `#0F6E56` vs `#E7F1ED` → ~5.5:1, проходит AA для non-text.
- **Pa11y** (`npm run a11y`): прогон только если route доступен без auth. Иначе skip с явной отметкой в plan. Не верифицировал на момент написания спеки: текущий запуск Pa11y, route guard конфиг.

### 4.4. Lint / Type-check

- `npm run lint:vue` — ESLint flat-config 10 + plugin-vue 10 (не верифицировал: правила vs новые `:deep()` селекторы и `class binding`; expected pass).
- `npm run type-check` — vue-tsc на src; `variant="outlined"` / `density="compact"` / `rounded="lg"` — стандартные Vuetify-3 props, types есть в `@types/vuetify` (входит в пакет).

### 4.5. Manual smoke

- Запустить `php artisan serve` + `npm run dev` параллельно (см. memory `feedback_environment.md` Plan 5 quirks).
- Авторизоваться (см. memory `project_supplier_integration.md` для demo-tenant credentials).
- `/projects` — проверить визуально: idle, hover, click (checked), keyboard Tab → focus-visible outline.
- Открыть «+ Создать проект» → визуально проверить outlined-поле «Название проекта» rest / hover / focus / error.

### 4.6. Pre-commit

`lefthook` stages (job'ы из репозитория — `lefthook.yml`):

- pint (PHP — не задеты)
- larastan (PHP — не задеты)
- squawk (миграции — не задеты)
- pgFormatter (SQL — не задеты)
- gitleaks (всегда)
- markdownlint (для spec.md этой задачи)
- ESLint (vue/ts — задеты оба файла)
- Pest (если в lefthook есть)

Полный прогон `lefthook run pre-commit` перед коммитом.

---

## 5. Out-of-scope (явно)

- НЕ редизайнится `.toolbar-check__box` в `ProjectsView.vue:50` — останется 2px-noir. Появится визуальная асимметрия (карточный checkbox ≠ toolbar checkbox). Заказчик принял.
- НЕ редизайнятся `v-combobox` (sms_senders, line 40), `v-autocomplete` (regions, line 70), `v-btn-toggle` (days, line 83) внутри `NewProjectDialog.vue` — останутся с дефолтным border-цветом темы.
- НЕ редизайнятся `v-select` в верхнем фильтр-баре `ProjectsView.vue` (lines 9, 19) и поисковый `v-text-field` (line 29). Скоуп user'ом обозначен как страничный диалог, не фильтр-бар.
- НЕ создаётся `LdTextField` / `LdCheckbox` primitive. Если в будущем потребуется унифицировать всё семейство — отдельный design / plan.
- НЕ обновляется `tokens.css`. Все правки — внутри scoped-стилей двух компонентов.
- НЕ обновляется `dev-indices.json` (signature и line-числа сохраняются).
- НЕ затрагивается логика валидации / emit'ов / store'а / API.

---

## 6. Риски и допущения

1. **`:deep()` против внутренних Vuetify-classes** — `.v-field__outline__start/end/notch::before/::after` зависит от внутренней Vuetify 3 SFC-структуры. Риск ломки при минорном bump'е Vuetify. Митигация: класс `.ld-input-quiet` локализует override, при обновлении Vuetify нужен manual recheck (не верифицировал точную совместимость с Vuetify 3.12 — assumption на основе официальной документации Vuetify 3.x).
2. **focus-visible на скрытом `<input>`** — input `position: absolute; opacity: 0; pointer-events: none`, но focusable (нет `tabindex="-1"`, нет `disabled`). `:focus-visible` должен срабатывать при Tab. Если в браузере Safari < 15.4 — `:focus-visible` не поддерживается, fallback на `:focus` для тех браузеров (не добавляем — рынок Safari < 15.4 ничтожен, см. caniuse).
3. ~~**`rounded="lg"` в Vuetify 3 = 8px**~~ — **снят**. Через `:deep(.v-field) { border-radius: var(--radius-8); }` фиксируем явное значение в CSS, не полагаясь на Vuetify internal mapping.
4. ~~**`color="primary"` подсветит label и caret в teal**~~ — **подтверждено** в Task 0.1: [vuetify.ts:19](../../app/resources/js/plugins/vuetify.ts#L19) определяет `primary: '#0F6E56'`, global default [vuetify.ts:53](../../app/resources/js/plugins/vuetify.ts#L53) задаёт `color=primary` для всех VTextField.
5. **Vitest snapshot-тесты, если есть** — могут зафейлиться на новом классе `ld-input-quiet` в render output. Task 0.3 показал, что snapshot-тестов нет; behavior-тесты не зависят от class. Митигация не нужна.
6. **Pre-existing baseline test failure** — `app/tests/Frontend/ProjectCard.spec.ts:43` (change-trigger на label вместо input) failing с момента создания spec'а (commit `c9ee8d8`). Фиксится отдельным `test(projects)` атомарным коммитом ДО Task 1, чтобы baseline был зелёным.

---

## 7. Definition of Done

- [ ] Pre-existing baseline test failure починен (`ProjectCard.spec.ts:43`, отдельный коммит).
- [ ] `ProjectCard.vue` `<style scoped>` заменён согласно §3.1.
- [ ] `NewProjectDialog.vue` 5 v-text-field получили `class="ld-input-quiet"` (lines 21, 30, 48, 59, 61) и добавлен `<style scoped>` блок согласно §3.2.
- [ ] Vitest зелёный (`npm run test:vue`, target 614 passed / 3 skipped / 0 failed).
- [ ] vue-tsc зелёный (`npm run type-check`).
- [ ] ESLint зелёный (`npm run lint:vue`).
- [ ] Manual smoke: все стейты #1440 + все стейты 5 v-text-field verified в браузере на dev-сервере.
- [ ] Histoire build (`npm run story:build`) green — story-файлы для обоих компонентов существуют.
- [ ] `lefthook run pre-commit` + `pre-push` зелёные.
- [ ] Атомарный коммит: «feat(projects-ui): Quiet Luxury redesign for card-check + 5 dialog v-text-fields».
- [ ] Push на ветку `plan5-frontend-projects` (текущая active).

---

## 8. Открытые вопросы — отсутствуют

В этой спецификации нет TBD / TODO / placeholder. Область фиксирована.
