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

Дата: 2026-05-12 Скоуп: буквально два элемента из Dev Element Indices. Цель: привести два визуально «тяжёлых» / выпадающих из Quiet Luxury (Portal Redesign I1) элемента к языку токенов `tokens.css`. Связанные документы: Portal Redesign — Quiet Luxury (I1), Dev Element Indices. 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.
- Сейчас: `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.
- Сейчас: дефолтный Vuetify 3 `<v-text-field>` (variant=filled по умолчанию) — серая плашка фоном, label inside, underline снизу.
- Проблема: filled-плашка Vuetify-defaults визуально чужая Quiet Luxury (нет 1px-language, нет `--liderra-surface` фона). Главное поле формы (после divider, имя проекта — visual anchor) выпадает из эстетики токенов.

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

- `.toolbar-check__box` в app/resources/js/views/ProjectsView.vue:50 — идентичные CSS-правила. Останется в текущем виде; следствие: после применения этой спеки на странице будет два разных стиля checkbox (карточка ≠ toolbar). Это осознанный trade-off.
- 4 других `v-text-field` в `NewProjectDialog.vue` (домен, номер, ключевое слово, лимит) — останутся в filled-варианте. Следствие: «Название проекта» станет визуальным акцентом — outlined среди filled. Это осознанный trade-off.

## 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) не сдвигается.
- `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):

    <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).
- Толщина 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 — расширение шаблона + новый `<style scoped>` в `NewProjectDialog.vue`

Template edit на line 59 — заменить:

    <v-text-field v-model="form.name" label="Название проекта" :error-messages="errors.name" />

на:

    <v-text-field
        v-model="form.name"
        label="Название проекта"
        variant="outlined"
        density="compact"
        color="primary"
        rounded="lg"
        class="ld-input-quiet"
        :error-messages="errors.name"
    />

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

    <style scoped>
    .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);
        transition: border-color 200ms cubic-bezier(0.16, 1, 0.3, 1);
    }
    .ld-input-quiet:hover :deep(.v-field__outline__start),
    .ld-input-quiet:hover :deep(.v-field__outline__end),
    .ld-input-quiet:hover :deep(.v-field__outline__notch::before),
    .ld-input-quiet:hover :deep(.v-field__outline__notch::after) {
        border-color: var(--liderra-line-strong);
    }
    .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);
    }
    </style>

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

- `variant="outlined"` — даёт 1px outline вместо filled-плашки.
- `density="compact"` — уменьшает высоту до ~40px (vs `default` ~56px), вписывается в общую плотность диалога.
- `color="primary"` — focused-стейт label / caret подсветится `var(--liderra-teal)` (тема `vuetify.ts` определяет primary=teal).
- `rounded="lg"` — Vuetify 3 mapping = 8px = `--radius-8`.
- `class="ld-input-quiet"` — уникальный класс, не зацепляет соседние 4 v-text-field.
- `:deep()` нужен потому что Vuetify-classes находятся в shadow-children компонента (Vue scoped-стили не пробивают границу без `:deep()`).
- `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) и 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). Заказчик принял.
- НЕ редизайнятся 4 остальных `v-text-field` в `NewProjectDialog.vue` (lines 21, 30, 48, 61). Останется filled-стиль. Заказчик принял.
- НЕ редизайнятся `v-select` в верхнем фильтр-баре `ProjectsView.vue` (lines 9, 19) и поисковый `v-text-field` (line 29). Скоуп user'ом обозначен как «буквально #1440 и #896».
- НЕ создаётся `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 — допущение по официальной документации `rounded` prop'а. Не верифицировал в коде Vuetify 3.12; если значение отличается, override через CSS `:deep(.v-field) { border-radius: var(--radius-8); }`.
4. `color="primary"` подсветит label и caret в teal — допущение на основе того, что `vuetify.ts` определяет `primary: var(--liderra-teal)`. Re-Read `app/resources/js/plugins/vuetify.ts` обязателен в implementation phase.
5. Vitest snapshot-тесты, если есть — могут зафейлиться на новой структуре props (`variant="outlined"` добавит классы Vuetify в render output). Митигация: обновить snapshot'ы в plan task'е, не считать regression'ом.

## 7. Definition of Done

- [ ] `ProjectCard.vue` `<style scoped>` заменён согласно §3.1.
- [ ] `NewProjectDialog.vue` template line 59 расширен и добавлен `<style scoped>` блок согласно §3.2.
- [ ] Vitest зелёный (`npm run test:vue`).
- [ ] vue-tsc зелёный (`npm run type-check`).
- [ ] ESLint зелёный (`npm run lint:vue`).
- [ ] Manual smoke: idle / hover / focus / checked / error verified в браузере на dev-сервере.
- [ ] Histoire не падает (если story есть).
- [ ] `lefthook run pre-commit` зелёный (gitleaks + markdownlint на этой спеке + ESLint).
- [ ] Атомарный коммит: «feat(projects-ui): Quiet Luxury редизайн card-check + name-field (#1440, #896)».
- [ ] Push на ветку `plan5-frontend-projects` (текущая active).

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

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