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-visibleoutline (раньше отсутствовал — 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 (vsdefault~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 на этом фоне:#0F6E56vs#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параллельно (см. memoryfeedback_environment.mdPlan 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/LdCheckboxprimitive. Если в будущем потребуется унифицировать всё семейство — отдельный design / plan. - НЕ обновляется
tokens.css. Все правки — внутри scoped-стилей двух компонентов. - НЕ обновляется
dev-indices.json(signature и line-числа сохраняются). - НЕ затрагивается логика валидации / emit'ов / store'а / API.
6. Риски и допущения
: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).- focus-visible на скрытом
<input>— inputposition: absolute; opacity: 0; pointer-events: none, но focusable (нетtabindex="-1", нетdisabled).:focus-visibleдолжен срабатывать при Tab. Если в браузере Safari < 15.4 —:focus-visibleне поддерживается, fallback на:focusдля тех браузеров (не добавляем — рынок Safari < 15.4 ничтожен, см. caniuse). rounded="lg"в Vuetify 3 = 8px — допущение по официальной документацииroundedprop'а. Не верифицировал в коде Vuetify 3.12; если значение отличается, override через CSS:deep(.v-field) { border-radius: var(--radius-8); }.color="primary"подсветит label и caret в teal — допущение на основе того, чтоvuetify.tsопределяетprimary: var(--liderra-teal). Re-Readapp/resources/js/plugins/vuetify.tsобязателен в implementation phase.- Vitest snapshot-тесты, если есть — могут зафейлиться на новой структуре props (
variant="outlined"добавит классы Vuetify в render output). Митигация: обновить snapshot'ы в plan task'е, не считать regression'ом.
7. Definition of Done
- [ ]
ProjectCard.vue<style scoped>заменён согласно §3.1. - [ ]
NewProjectDialog.vuetemplate 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. Область фиксирована.