2026-05-11 19:30:45 +03:00
|
|
|
|
<template>
|
2026-05-22 18:50:04 +03:00
|
|
|
|
<v-container fluid class="projects-view" :class="{ 'has-drawer': singleSelectedProject !== null }">
|
2026-05-11 19:38:59 +03:00
|
|
|
|
<div class="d-flex justify-space-between align-center mb-4">
|
|
|
|
|
|
<h1 class="text-h4">Проекты</h1>
|
2026-06-21 12:16:45 +03:00
|
|
|
|
<v-btn color="primary" prepend-icon="mdi-plus" @click="openCreate">Создать проект</v-btn>
|
2026-05-11 19:38:59 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-05-21 11:17:04 +03:00
|
|
|
|
<v-alert
|
2026-06-24 16:35:20 +03:00
|
|
|
|
v-if="showCutoffBanner && !isEmptyAccount"
|
2026-05-21 11:17:04 +03:00
|
|
|
|
data-testid="cutoff-banner"
|
|
|
|
|
|
type="info"
|
|
|
|
|
|
variant="tonal"
|
|
|
|
|
|
border="start"
|
|
|
|
|
|
class="mb-4"
|
|
|
|
|
|
>
|
2026-06-21 12:44:19 +03:00
|
|
|
|
<div class="d-flex justify-space-between align-start ga-2">
|
2026-05-21 11:17:04 +03:00
|
|
|
|
<span>
|
2026-06-25 12:02:52 +03:00
|
|
|
|
Изменения по проектам (лимиты, регионы, дни) вступают в силу со следующего дня. Внесённые
|
|
|
|
|
|
<strong>до 18:00 МСК</strong> — начнут работать уже завтра, после 18:00 — послезавтра.
|
2026-05-21 11:17:04 +03:00
|
|
|
|
</span>
|
|
|
|
|
|
<v-btn
|
|
|
|
|
|
data-testid="cutoff-banner-close"
|
|
|
|
|
|
icon="mdi-close"
|
|
|
|
|
|
size="x-small"
|
|
|
|
|
|
variant="text"
|
|
|
|
|
|
aria-label="Скрыть уведомление"
|
|
|
|
|
|
@click="dismissCutoffBanner"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</v-alert>
|
|
|
|
|
|
|
2026-06-24 16:35:20 +03:00
|
|
|
|
<div v-if="!isEmptyAccount" class="d-flex flex-wrap ga-3 mb-4">
|
2026-05-11 19:38:59 +03:00
|
|
|
|
<v-select
|
|
|
|
|
|
v-model="store.filters.signal_type"
|
|
|
|
|
|
:items="typeFilters"
|
|
|
|
|
|
label="Тип"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
density="comfortable"
|
|
|
|
|
|
style="max-width: 180px"
|
|
|
|
|
|
hide-details
|
|
|
|
|
|
@update:model-value="store.fetch()"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<v-select
|
|
|
|
|
|
v-model="store.filters.status"
|
|
|
|
|
|
:items="statusFilters"
|
|
|
|
|
|
label="Статус"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
density="comfortable"
|
|
|
|
|
|
style="max-width: 180px"
|
|
|
|
|
|
hide-details
|
|
|
|
|
|
@update:model-value="store.fetch()"
|
|
|
|
|
|
/>
|
2026-05-22 18:50:04 +03:00
|
|
|
|
<v-autocomplete
|
|
|
|
|
|
v-model="store.filters.region"
|
|
|
|
|
|
:items="regionFilterItems"
|
|
|
|
|
|
item-title="title"
|
|
|
|
|
|
item-value="value"
|
|
|
|
|
|
label="Регион"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
density="comfortable"
|
|
|
|
|
|
style="max-width: 240px"
|
|
|
|
|
|
hide-details
|
|
|
|
|
|
data-testid="filter-region"
|
|
|
|
|
|
@update:model-value="onResetPageAndFetch()"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<v-select
|
|
|
|
|
|
v-model="store.filters.delivery_day"
|
|
|
|
|
|
:items="dayFilterItems"
|
|
|
|
|
|
item-title="title"
|
|
|
|
|
|
item-value="value"
|
|
|
|
|
|
label="День приёма"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
density="comfortable"
|
|
|
|
|
|
style="max-width: 180px"
|
|
|
|
|
|
hide-details
|
|
|
|
|
|
data-testid="filter-day"
|
|
|
|
|
|
@update:model-value="onResetPageAndFetch()"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<v-select
|
|
|
|
|
|
v-model="store.filters.sort"
|
|
|
|
|
|
:items="sortItems"
|
|
|
|
|
|
item-title="title"
|
|
|
|
|
|
item-value="value"
|
|
|
|
|
|
label="Сортировать"
|
|
|
|
|
|
density="comfortable"
|
|
|
|
|
|
style="max-width: 240px"
|
|
|
|
|
|
hide-details
|
|
|
|
|
|
data-testid="filter-sort"
|
|
|
|
|
|
@update:model-value="onResetPageAndFetch()"
|
|
|
|
|
|
/>
|
2026-05-11 19:38:59 +03:00
|
|
|
|
<v-text-field
|
|
|
|
|
|
v-model="store.filters.search"
|
|
|
|
|
|
label="Поиск"
|
|
|
|
|
|
prepend-inner-icon="mdi-magnify"
|
|
|
|
|
|
density="comfortable"
|
|
|
|
|
|
hide-details
|
|
|
|
|
|
style="max-width: 240px"
|
|
|
|
|
|
@input="onSearchDebounced"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-05-22 18:50:04 +03:00
|
|
|
|
<!-- #6: Селектор количества на странице (паттерн как в DealsView). -->
|
2026-06-24 16:35:20 +03:00
|
|
|
|
<div v-if="!isEmptyAccount" class="perpage mb-3 d-flex align-center ga-3">
|
2026-05-22 18:50:04 +03:00
|
|
|
|
<span class="text-body-2 text-medium-emphasis">Показывать по:</span>
|
|
|
|
|
|
<v-btn-toggle
|
|
|
|
|
|
v-model="store.filters.per_page"
|
|
|
|
|
|
mandatory
|
|
|
|
|
|
density="comfortable"
|
|
|
|
|
|
variant="outlined"
|
|
|
|
|
|
color="primary"
|
|
|
|
|
|
data-testid="perpage-toggle"
|
|
|
|
|
|
@update:model-value="onResetPageAndFetch()"
|
|
|
|
|
|
>
|
|
|
|
|
|
<v-btn v-for="n in PER_PAGE_OPTIONS" :key="n" :value="n" size="small">{{ n }}</v-btn>
|
|
|
|
|
|
</v-btn-toggle>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-06-21 12:44:19 +03:00
|
|
|
|
<div v-if="!store.loading && store.items.length > 0" class="projects-toolbar d-flex align-center ga-3 mb-3">
|
2026-05-12 19:25:25 +03:00
|
|
|
|
<label class="toolbar-check" data-testid="select-all-toggle">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
2026-05-12 22:09:48 +03:00
|
|
|
|
aria-label="Выбрать все проекты по текущим фильтрам"
|
2026-05-12 19:25:25 +03:00
|
|
|
|
:checked="store.selectAllByFilter"
|
|
|
|
|
|
@change="(e) => onToggleSelectAll((e.target as HTMLInputElement).checked)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span
|
|
|
|
|
|
class="toolbar-check__box"
|
|
|
|
|
|
:class="{ 'toolbar-check__box--partial': !store.selectAllByFilter && store.selectedIds.size > 0 }"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</label>
|
2026-05-12 20:24:33 +03:00
|
|
|
|
<span class="text-body-2"
|
|
|
|
|
|
>Выбрано: {{ store.selectedIds.size }} из {{ store.total }} (по текущим фильтрам)</span
|
|
|
|
|
|
>
|
2026-05-12 15:11:21 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-05-11 19:38:59 +03:00
|
|
|
|
<div v-if="store.loading" class="text-center py-8">
|
|
|
|
|
|
<v-progress-circular indeterminate color="primary" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else-if="store.items.length === 0" class="text-center py-12 text-medium-emphasis">
|
|
|
|
|
|
Нет проектов. Создайте первый — кнопка справа сверху.
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="projects-grid">
|
|
|
|
|
|
<ProjectCard
|
|
|
|
|
|
v-for="project in store.items"
|
|
|
|
|
|
:key="project.id"
|
|
|
|
|
|
:project="project"
|
|
|
|
|
|
:selected="store.selectedIds.has(project.id)"
|
|
|
|
|
|
@toggle-select="store.toggleSelect"
|
|
|
|
|
|
@edit="openEdit"
|
|
|
|
|
|
@toggle-active="store.toggleActive"
|
|
|
|
|
|
@sync-now="(p: Project) => store.syncNow(p.id)"
|
2026-05-21 08:37:26 +03:00
|
|
|
|
@delete="(p: Project) => store.del(p.id)"
|
2026-05-11 19:38:59 +03:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-05-22 18:50:04 +03:00
|
|
|
|
<!-- #6: Пагинация для случаев когда total > per_page. -->
|
|
|
|
|
|
<div v-if="pageCount > 1" class="d-flex justify-center mt-4">
|
|
|
|
|
|
<v-pagination
|
|
|
|
|
|
v-model="store.filters.page"
|
|
|
|
|
|
:length="pageCount"
|
|
|
|
|
|
:total-visible="7"
|
|
|
|
|
|
density="comfortable"
|
|
|
|
|
|
data-testid="projects-pagination"
|
|
|
|
|
|
@update:model-value="store.fetch()"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-06-23 09:06:46 +03:00
|
|
|
|
<!-- v-show (не v-if): после bulk-действия store.bulkUpdate сбрасывает выбор → при v-if
|
|
|
|
|
|
бар размонтировался бы вместе со snackbar'ом и тост о пропущенных (в т.ч. «не хватает
|
|
|
|
|
|
баланса») никогда не показывался. v-show держит компонент смонтированным; snackbar
|
|
|
|
|
|
Vuetify телепортируется в body и виден даже при скрытом баре. -->
|
|
|
|
|
|
<BulkActionsBar v-show="store.selectedIds.size >= 2" />
|
2026-05-14 15:02:33 +03:00
|
|
|
|
|
2026-06-17 05:17:12 +03:00
|
|
|
|
<ProjectDetailsDrawer :project="singleSelectedProject" @close="onDrawerClose" @saved="onDrawerSaved" />
|
2026-05-28 09:52:42 +03:00
|
|
|
|
<NewProjectDialog v-model="createOpen" mode="create" @saved="onProjectSaved" />
|
|
|
|
|
|
<EditProjectDialog v-model="editOpen" :project="editing" @saved="onProjectSaved" />
|
|
|
|
|
|
|
|
|
|
|
|
<v-snackbar
|
|
|
|
|
|
v-model="savedSnackbarOpen"
|
|
|
|
|
|
color="success"
|
|
|
|
|
|
:timeout="appliesFromShown ? 7000 : 3500"
|
|
|
|
|
|
data-testid="projects-saved-snackbar"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ savedSnackbarMessage }}
|
|
|
|
|
|
</v-snackbar>
|
2026-05-22 18:50:04 +03:00
|
|
|
|
</v-container>
|
2026-05-11 19:30:45 +03:00
|
|
|
|
</template>
|
2026-05-11 19:38:59 +03:00
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-05-14 15:02:33 +03:00
|
|
|
|
import { ref, onMounted, onUnmounted, watch, computed } from 'vue';
|
2026-05-11 19:38:59 +03:00
|
|
|
|
import { useProjectsStore, type Project } from '../stores/projectsStore';
|
|
|
|
|
|
import ProjectCard from '../components/projects/ProjectCard.vue';
|
2026-05-14 15:02:33 +03:00
|
|
|
|
import ProjectDetailsDrawer from '../components/projects/ProjectDetailsDrawer.vue';
|
2026-05-11 19:38:59 +03:00
|
|
|
|
import BulkActionsBar from '../components/projects/BulkActionsBar.vue';
|
|
|
|
|
|
import NewProjectDialog from './projects/NewProjectDialog.vue';
|
|
|
|
|
|
import EditProjectDialog from './projects/EditProjectDialog.vue';
|
2026-05-22 18:50:04 +03:00
|
|
|
|
import { REGIONS } from '../constants/regions';
|
2026-05-28 09:52:42 +03:00
|
|
|
|
import { formatAppliesFromMessage } from '../composables/appliesFromMessage';
|
2026-05-11 19:38:59 +03:00
|
|
|
|
|
|
|
|
|
|
const store = useProjectsStore();
|
|
|
|
|
|
const createOpen = ref(false);
|
|
|
|
|
|
const editOpen = ref(false);
|
|
|
|
|
|
const editing = ref<Project | null>(null);
|
|
|
|
|
|
|
2026-05-28 09:52:42 +03:00
|
|
|
|
// Тост «Сохранено» после правки проекта. Если правка задела slepok-чувствительные
|
|
|
|
|
|
// поля (regions / delivery_days_mask / daily_limit_target / источник), backend
|
|
|
|
|
|
// возвращает applies_from = N.21:00 МСК — показываем расширенное сообщение.
|
|
|
|
|
|
const savedSnackbarOpen = ref(false);
|
|
|
|
|
|
const savedSnackbarMessage = ref('');
|
|
|
|
|
|
const appliesFromShown = ref(false);
|
|
|
|
|
|
function showSavedSnackbar(appliesFrom: string | null): void {
|
|
|
|
|
|
savedSnackbarMessage.value = formatAppliesFromMessage(appliesFrom);
|
|
|
|
|
|
appliesFromShown.value = appliesFrom !== null;
|
|
|
|
|
|
savedSnackbarOpen.value = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 11:17:04 +03:00
|
|
|
|
// Информационный баннер о сроке внесения изменений (синхронизация с поставщиком в 18:00 МСК).
|
|
|
|
|
|
// Закрытие запоминается, чтобы не показывать повторно.
|
|
|
|
|
|
const CUTOFF_BANNER_KEY = 'projects.cutoffBannerDismissed';
|
|
|
|
|
|
const showCutoffBanner = ref(localStorage.getItem(CUTOFF_BANNER_KEY) !== '1');
|
|
|
|
|
|
function dismissCutoffBanner(): void {
|
|
|
|
|
|
showCutoffBanner.value = false;
|
|
|
|
|
|
localStorage.setItem(CUTOFF_BANNER_KEY, '1');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-24 16:35:20 +03:00
|
|
|
|
// Косяк 07: «пустой аккаунт» — ни одного проекта И фильтры не сужают выдачу.
|
|
|
|
|
|
// При нём прячем баннер «до 18:00» и фильтры/пагинацию, чтобы новичок не тонул
|
|
|
|
|
|
// в шуме (если фильтр активен и дал 0 — фильтры остаются, чтобы их сбросить).
|
|
|
|
|
|
const anyFilterActive = computed(() => {
|
|
|
|
|
|
const f = store.filters;
|
|
|
|
|
|
return !!f.signal_type || !!f.status || f.region != null || f.delivery_day != null || !!String(f.search ?? '').trim();
|
|
|
|
|
|
});
|
|
|
|
|
|
const isEmptyAccount = computed(() => !store.loading && store.total === 0 && !anyFilterActive.value);
|
|
|
|
|
|
|
2026-05-14 15:02:33 +03:00
|
|
|
|
const singleSelectedProject = computed<Project | null>(() => {
|
|
|
|
|
|
if (store.selectedIds.size !== 1) return null;
|
|
|
|
|
|
const [id] = store.selectedIds;
|
|
|
|
|
|
return store.items.find((p: Project) => p.id === id) ?? null;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
function onDrawerClose(): void {
|
|
|
|
|
|
store.clearSelection();
|
|
|
|
|
|
}
|
2026-05-28 09:52:42 +03:00
|
|
|
|
function onDrawerSaved(appliesFrom: string | null): void {
|
2026-05-22 18:50:04 +03:00
|
|
|
|
// #4: после Save/Pause/Delete панель и галочка должны исчезнуть.
|
|
|
|
|
|
store.clearSelection();
|
2026-05-14 15:02:33 +03:00
|
|
|
|
void store.fetch();
|
2026-05-28 09:52:42 +03:00
|
|
|
|
showSavedSnackbar(appliesFrom);
|
|
|
|
|
|
}
|
|
|
|
|
|
function onProjectSaved(appliesFrom: string | null): void {
|
|
|
|
|
|
void store.fetch();
|
|
|
|
|
|
showSavedSnackbar(appliesFrom);
|
2026-05-14 15:02:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-11 19:38:59 +03:00
|
|
|
|
const typeFilters = [
|
|
|
|
|
|
{ title: 'Сайт', value: 'site' },
|
|
|
|
|
|
{ title: 'Звонок', value: 'call' },
|
|
|
|
|
|
{ title: 'СМС', value: 'sms' },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const statusFilters = [
|
|
|
|
|
|
{ title: 'Активные', value: 'active' },
|
|
|
|
|
|
{ title: 'На паузе', value: 'paused' },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2026-05-22 18:50:04 +03:00
|
|
|
|
// #6: per_page options (паттерн как в DealsView).
|
|
|
|
|
|
const PER_PAGE_OPTIONS = [20, 50, 100, 200];
|
|
|
|
|
|
|
|
|
|
|
|
// #7: фильтр регион — все 89 субъектов (без code=0 «вся РФ» — пустой regions[] и так попадает в выдачу).
|
|
|
|
|
|
const regionFilterItems = REGIONS.filter((r) => r.code !== 0).map((r) => ({ title: r.name, value: r.code }));
|
|
|
|
|
|
|
|
|
|
|
|
// #7: фильтр по дню недели — индексы совпадают с битами delivery_days_mask и dayLabels drawer'а.
|
|
|
|
|
|
const dayFilterItems = [
|
|
|
|
|
|
{ title: 'Пн', value: 0 },
|
|
|
|
|
|
{ title: 'Вт', value: 1 },
|
|
|
|
|
|
{ title: 'Ср', value: 2 },
|
|
|
|
|
|
{ title: 'Чт', value: 3 },
|
|
|
|
|
|
{ title: 'Пт', value: 4 },
|
|
|
|
|
|
{ title: 'Сб', value: 5 },
|
|
|
|
|
|
{ title: 'Вс', value: 6 },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
// #7: сортировки. Префикс '-' = по убыванию (новые/больше — сверху).
|
|
|
|
|
|
const sortItems = [
|
|
|
|
|
|
{ title: 'Лидов сегодня — сначала больше', value: '-delivered_today' },
|
|
|
|
|
|
{ title: 'Лидов за месяц — сначала больше', value: '-delivered_in_month' },
|
|
|
|
|
|
{ title: 'Лимит — сначала больше', value: '-daily_limit_target' },
|
|
|
|
|
|
{ title: 'Лимит — сначала меньше', value: 'daily_limit_target' },
|
|
|
|
|
|
{ title: 'Название — А → Я', value: 'name' },
|
|
|
|
|
|
{ title: 'Название — Я → А', value: '-name' },
|
|
|
|
|
|
{ title: 'Создан — сначала новые', value: '-created_at' },
|
|
|
|
|
|
{ title: 'Создан — сначала старые', value: 'created_at' },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
// #6: общее число страниц для пагинатора.
|
2026-06-17 05:17:12 +03:00
|
|
|
|
const pageCount = computed(() => Math.max(1, Math.ceil(store.total / Math.max(1, store.filters.per_page))));
|
2026-05-22 18:50:04 +03:00
|
|
|
|
|
|
|
|
|
|
// При смене per_page/sort/region/delivery_day — сброс на 1-ю страницу + fetch.
|
|
|
|
|
|
function onResetPageAndFetch(): void {
|
|
|
|
|
|
store.filters.page = 1;
|
|
|
|
|
|
void store.fetch();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-11 19:38:59 +03:00
|
|
|
|
let searchTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
|
|
function onSearchDebounced() {
|
|
|
|
|
|
if (searchTimer) clearTimeout(searchTimer);
|
|
|
|
|
|
searchTimer = setTimeout(() => store.fetch(), 300);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openCreate() {
|
|
|
|
|
|
createOpen.value = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
function openEdit(project: Project) {
|
|
|
|
|
|
editing.value = project;
|
|
|
|
|
|
editOpen.value = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 15:11:21 +03:00
|
|
|
|
function onToggleSelectAll(value: boolean | null) {
|
|
|
|
|
|
if (value) {
|
|
|
|
|
|
store.selectAllByFilter = true;
|
|
|
|
|
|
store.items.forEach((p: Project) => store.selectedIds.add(p.id));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
store.selectAllByFilter = false;
|
|
|
|
|
|
store.clearSelection();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-11 19:44:56 +03:00
|
|
|
|
watch(
|
|
|
|
|
|
() => store.pendingIds.size,
|
|
|
|
|
|
(size) => {
|
|
|
|
|
|
if (size > 0) store.startPolling();
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-05-11 19:38:59 +03:00
|
|
|
|
onMounted(store.fetch);
|
2026-05-11 19:44:56 +03:00
|
|
|
|
onUnmounted(() => store.stopPolling());
|
2026-05-11 19:38:59 +03:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.projects-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
}
|
2026-05-12 19:25:25 +03:00
|
|
|
|
.toolbar-check {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
padding: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.toolbar-check input {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
.toolbar-check__box {
|
|
|
|
|
|
width: 20px;
|
|
|
|
|
|
height: 20px;
|
|
|
|
|
|
border: 2px solid var(--liderra-noir);
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
transition: background 150ms ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
.toolbar-check input:checked + .toolbar-check__box {
|
2026-05-12 20:24:33 +03:00
|
|
|
|
background: var(--liderra-teal, #0f6e56);
|
|
|
|
|
|
border-color: var(--liderra-teal, #0f6e56);
|
2026-05-12 19:25:25 +03:00
|
|
|
|
}
|
|
|
|
|
|
.toolbar-check input:checked + .toolbar-check__box::after {
|
|
|
|
|
|
content: '';
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
left: 5px;
|
|
|
|
|
|
top: 1px;
|
|
|
|
|
|
width: 6px;
|
|
|
|
|
|
height: 11px;
|
|
|
|
|
|
border: solid #fff;
|
|
|
|
|
|
border-width: 0 2px 2px 0;
|
|
|
|
|
|
transform: rotate(45deg);
|
|
|
|
|
|
}
|
|
|
|
|
|
.toolbar-check__box--partial {
|
2026-05-12 20:24:33 +03:00
|
|
|
|
background: var(--liderra-teal, #0f6e56);
|
|
|
|
|
|
border-color: var(--liderra-teal, #0f6e56);
|
2026-05-12 19:25:25 +03:00
|
|
|
|
}
|
|
|
|
|
|
.toolbar-check__box--partial::after {
|
|
|
|
|
|
content: '';
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
left: 3px;
|
|
|
|
|
|
top: 7px;
|
|
|
|
|
|
width: 10px;
|
|
|
|
|
|
height: 2px;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
}
|
2026-05-22 18:50:04 +03:00
|
|
|
|
/* #5: отступ от тёмных границ страницы — как в KanbanView (pa-6 = 24px по всем сторонам).
|
|
|
|
|
|
Скоупим напрямую вместо utility-класса, чтобы has-drawer мог перекрыть правый отступ. */
|
2026-05-14 15:02:33 +03:00
|
|
|
|
.projects-view {
|
2026-05-22 18:50:04 +03:00
|
|
|
|
padding: 24px;
|
2026-05-14 15:02:33 +03:00
|
|
|
|
transition: padding-right 240ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
.projects-view.has-drawer {
|
|
|
|
|
|
padding-right: 480px;
|
|
|
|
|
|
}
|
2026-05-11 19:38:59 +03:00
|
|
|
|
</style>
|