Files
portal/app/resources/js/plugins/vuetify.ts
T
Дмитрий 3d728a2bab fix(реклама): пачка 5 — приёмка глазами: 20 значков рисовались вопросом в кружке
Приёмка обоих рекламных экранов на локальном портале. Экраны работают:
живой счётчик охвата правда ходит на сервер, ручка оценки правда только
читает (после её вызовов ноль черновиков в базе), заголовок объявления сам
появляется при ссылке на сайт, «Запустить» не нажимается при охвате меньше
367, «Принято, показов пока нет» и «часть объявлений отклонена — 2 из 15»
на месте.

Находка, которой не видел ни один из 1829 зелёных тестов: значки в портале
не встроенные, имена mdi-* вручную сопоставлены с набором Lucide, а
незнакомое имя молча подменяется на HelpCircle. Клиент видит вопрос в
кружке вместо значка. Замер: 43 имени из 162 по всему порталу.

На рекламных экранах таких было 20 — включая заголовок телеграм-экрана,
ярлыки «Проверяем вручную» и «Запущена в кабинете МТС», поле «Картинка или
видео» и ярлык «Черновик» у Яндекса. Шесть из них добавил я в пачках 1–4,
четырнадцать ломались раньше. Все 20 починены, проверено глазами после
пересборки.

Заведён сторож tests/Frontend/znachki-est-v-karte.spec.ts: краснеет на
любом имени вне карты и называет файл-виновника. До починки был красным
ровно на тех 20 — проверено вырезанием. Оставшиеся 23 сломанных имени на
экранах вне рекламы записаны в нём списком известного долга; третья
проверка следит, чтобы список не протух.

Карта значков открыта наружу (export) ради сторожа — поведение не меняется.

Замеры: экраны 241 файл / 1832 теста / 0 падений (было 240/1829),
Advertising 435, ClientTg 292, статанализ 0, pint чисто, типы — те же 6
чужих ошибок. На боевой ничего не катал.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 19:14:11 +03:00

383 lines
12 KiB
TypeScript

// @ts-expect-error vuetify/styles — CSS-импорт без d.ts
import 'vuetify/styles';
import { h, type Component } from 'vue';
import { createVuetify } from 'vuetify';
import { ru } from 'vuetify/locale';
import type { ThemeDefinition, IconSet, IconProps } from 'vuetify';
import {
Activity,
AlertCircle,
AlertTriangle,
Archive,
ArrowDown,
ArrowLeft,
ArrowRightLeft,
ArrowUp,
Ban,
Bell,
BellOff,
Bot,
Calendar,
CalendarClock,
CalendarDays,
CalendarRange,
Camera,
Check,
CheckCheck,
CheckCircle,
ChevronDown,
ChevronLeft,
ChevronRight,
ChevronsLeft,
ChevronsRight,
ChevronsUpDown,
ChevronUp,
Circle,
CircleDot,
CirclePause,
CirclePlay,
CircleStop,
Clock,
Code,
Columns3,
Copy,
CreditCard,
Download,
Eye,
EyeOff,
FilterX,
FileCheck,
FileSpreadsheet,
FileText,
FileUp,
FlaskConical,
Folder,
Folders,
Globe,
HelpCircle,
Image as ImageIcon,
Info,
Key,
KeyRound,
LayoutDashboard,
LifeBuoy,
Link2,
List,
LogOut,
Mail,
Megaphone,
Menu,
MessageSquare,
MessageSquareText,
Minus,
MoreVertical,
Paperclip,
Pause,
Pencil,
Phone,
Play,
Plus,
PlusCircle,
Puzzle,
ReceiptText,
RefreshCw,
Repeat,
Repeat2,
Rocket,
RotateCcw,
RotateCw,
RussianRuble,
Save,
Search,
Send,
SendHorizontal,
Settings,
Shield,
ShieldCheck,
ShieldOff,
Square,
SquareCheck,
SquareMinus,
Star,
StarHalf,
Tag,
Target,
Trash2,
Upload,
User,
UserCheck,
UserCog,
UserPlus,
Users,
UserSearch,
UserX,
Wallet,
Webhook,
X,
XCircle,
} from 'lucide-vue-next';
/**
* Палитра Forest extended (Iteration 1 — Quiet Luxury redesign).
* Spec: docs/superpowers/specs/2026-05-12-portal-redesign-quiet-luxury-design.md §3
* CSS-токены: app/resources/css/tokens.css (single source of truth)
*
* 14 OKLCH-статусов воронки маппятся на slugs из db/schema.sql:2076 (lead_statuses)
* через `useStatusPill` composable, НЕ через Vuetify theme.
*/
const liderraForest: ThemeDefinition = {
dark: false,
colors: {
background: '#F6F3EC',
surface: '#FFFFFF',
primary: '#0F6E56',
'on-primary': '#FFFFFF',
secondary: '#012019',
'on-secondary': '#F6F3EC',
success: '#2E8B57',
warning: '#D9A441',
// Текстовые двойники success/warning. Сами брендовые цвета не трогаем —
// они остаются для заливок, иконок и рамок, где 4.5:1 не требуется. Но
// ТЕКСТОМ они нечитаемы: success даёт 4.25:1 на белой карточке и 3.83:1
// на ивори #F6F3EC, warning — 2.25/2.03 при норме WCAG 2.1 AA 4.5:1.
// Затемнённые двойники: 6.10/5.51 и 5.37/4.85 — тон сохранён.
// Использовать как text-success-strong / text-warning-strong.
'success-strong': '#256F46',
'warning-strong': '#8A6410',
error: '#B83A3A',
info: '#3F7C95',
// Расширения — для data viz и semantic uses
'liderra-plum': '#7A5BA3',
'liderra-salmon': '#CC6E50',
'liderra-teal-deep': '#0A5A47',
'liderra-muted': '#6B6356',
},
};
/**
* Liderra Lucide IconSet (CTO-19 closure, v1.83).
* Spec: docs/superpowers/specs/2026-05-13-cto-19-lucide-icon-migration-design.md
*
* Maps 80 mdi-* semantic-ID strings (used across 52 Vue/TS files in resources/js/)
* to Lucide Vue components for rendering. Views НЕ touched — mdi-* строки остаются
* как semantic identifiers, рендерятся через Lucide stroke-based SVG icons.
*
* CLAUDE.md §2 «Иконки: Lucide» — бренд-spec compliance.
*
* Экспортируется наружу РАДИ СТОРОЖА: имя, которого тут нет, молча подменяется
* на HelpCircle (см. ниже), и на экране вместо значка появляется вопрос в кружке.
* Замер 02.08.2026: так молча ломались 43 имени из 162. Сторож —
* tests/Frontend/znachki-est-v-karte.spec.ts.
*/
export const lucideMap: Record<string, Component> = {
'mdi-account-arrow-right-outline': UserCheck,
'mdi-account-group-outline': Users,
'mdi-account-outline': User,
'mdi-account-plus-outline': UserPlus,
'mdi-account-switch': UserCog,
'mdi-alert-circle': AlertCircle,
'mdi-alert-circle-outline': AlertCircle,
'mdi-alert-outline': AlertTriangle,
'mdi-api': Webhook,
'mdi-archive': Archive,
'mdi-arrow-down': ArrowDown,
'mdi-arrow-left': ArrowLeft,
'mdi-arrow-up': ArrowUp,
'mdi-autorenew': RotateCw,
'mdi-bell-off-outline': BellOff,
'mdi-bell-outline': Bell,
'mdi-bullhorn-outline': Megaphone,
'mdi-camera': Camera,
'mdi-cash-plus': PlusCircle,
'mdi-chart-box-outline': LayoutDashboard,
'mdi-check': Check,
'mdi-check-circle': CheckCircle,
'mdi-check-circle-outline': CheckCircle,
'mdi-chevron-right': ChevronRight,
'mdi-clock-check-outline': Clock,
'mdi-clock-outline': Clock,
'mdi-close': X,
'mdi-close-circle': XCircle,
'mdi-cog-outline': Settings,
'mdi-comment-outline': MessageSquare,
'mdi-content-copy': Copy,
'mdi-content-save-outline': Save,
'mdi-credit-card-outline': CreditCard,
'mdi-currency-rub': RussianRuble,
'mdi-delete': Trash2,
'mdi-delete-outline': Trash2,
'mdi-dots-vertical': MoreVertical,
'mdi-download': Download,
'mdi-email-outline': Mail,
'mdi-eye': Eye,
'mdi-eye-off': EyeOff,
'mdi-eye-outline': Eye,
'mdi-file-pdf-box': FileText,
'mdi-filter-off': FilterX,
'mdi-folder-multiple-outline': Folders,
'mdi-folder-outline': Folder,
'mdi-format-list-bulleted': List,
'mdi-key': Key,
'mdi-lock-reset': KeyRound,
'mdi-logout': LogOut,
'mdi-magnify': Search,
'mdi-message-text': MessageSquareText,
'mdi-pause': Pause,
'mdi-pencil': Pencil,
'mdi-pencil-outline': Pencil,
'mdi-phone': Phone,
'mdi-play': Play,
'mdi-plus': Plus,
'mdi-plus-circle-outline': PlusCircle,
'mdi-progress-clock': Clock,
'mdi-pulse': Activity,
'mdi-puzzle-outline': Puzzle,
'mdi-receipt-text-check-outline': ReceiptText,
'mdi-refresh': RefreshCw,
'mdi-restore': RotateCcw,
'mdi-shield-account-outline': ShieldCheck,
'mdi-shield-key': Shield,
'mdi-shield-lock-outline': ShieldCheck,
'mdi-shield-off': ShieldOff,
'mdi-stop-circle-outline': CircleStop,
'mdi-swap-horizontal': ArrowRightLeft,
'mdi-tag-arrow-right': Tag,
'mdi-test-tube': FlaskConical,
'mdi-trash-can-outline': Trash2,
'mdi-view-column-outline': Columns3,
'mdi-view-dashboard-outline': LayoutDashboard,
'mdi-wallet-outline': Wallet,
'mdi-web': Globe,
'mdi-xml': Code,
// UI-аудит 21.06.2026 — иконки, ранее падавшие в HelpCircle-fallback («?»):
// bulk-бар проектов, импорт, экспорт отчётов/сделок, помощь, действия админки.
'mdi-account-cancel': UserX,
'mdi-cash-edit': Pencil,
'mdi-cash-refund': RotateCcw,
'mdi-database-import-outline': Upload,
'mdi-delete-forever': Trash2,
'mdi-earth': Globe,
'mdi-file-delimited-outline': FileText,
'mdi-file-document-outline': FileText,
'mdi-file-excel-outline': FileSpreadsheet,
'mdi-format-list-checks': List,
'mdi-information-outline': Info,
'mdi-lifebuoy': LifeBuoy,
'mdi-target': Target,
// Раздел «Рекламные возможности» (витрина будущих каналов).
'mdi-robot-outline': Bot,
'mdi-send-outline': Send,
// Vuetify-internal default mdi-* aliases (CTO-19 closure extension)
'mdi-chevron-down': ChevronDown,
'mdi-chevron-left': ChevronLeft,
'mdi-menu-down': ChevronDown,
'mdi-menu-right': ChevronRight,
'mdi-menu-up': ChevronUp,
'mdi-menu': Menu,
'mdi-page-first': ChevronsLeft,
'mdi-page-last': ChevronsRight,
'mdi-checkbox-marked': SquareCheck,
'mdi-checkbox-blank-outline': Square,
'mdi-minus-box': SquareMinus,
'mdi-radiobox-marked': CircleDot,
'mdi-radiobox-blank': Circle,
'mdi-circle': Circle,
'mdi-information': Info,
'mdi-minus': Minus,
'mdi-calendar': Calendar,
'mdi-calendar-month': CalendarDays,
'mdi-paperclip': Paperclip,
'mdi-unfold-more-horizontal': ChevronsUpDown,
'mdi-window-close': X,
'mdi-cached': RefreshCw,
'mdi-star': Star,
'mdi-star-outline': Star,
'mdi-star-half-full': StarHalf,
// ——— Рекламные экраны (приёмка глазами 02.08.2026) ———
// Этих имён в карте не было, и на экране вместо значка стоял вопрос в
// кружке: у ярлыков «Проверяем вручную» и «Запущена в кабинете МТС», у поля
// «Картинка или видео», у заголовка телеграм-экрана. Ярлык состояния с
// вопросом вместо значка читается как «портал сам не знает, что происходит».
'mdi-account-search-outline': UserSearch,
'mdi-calendar-clock': CalendarClock,
'mdi-calendar-range': CalendarRange,
'mdi-cancel': Ban,
'mdi-check-all': CheckCheck,
'mdi-file-check-outline': FileCheck,
'mdi-file-outline': FileText,
'mdi-file-upload-outline': FileUp,
'mdi-help-circle-outline': HelpCircle,
'mdi-image-outline': ImageIcon,
'mdi-link-variant': Link2,
'mdi-pause-circle': CirclePause,
'mdi-play-circle': CirclePlay,
'mdi-repeat': Repeat,
'mdi-repeat-variant': Repeat2,
'mdi-rocket-launch-outline': Rocket,
'mdi-send-check-outline': SendHorizontal,
'mdi-send-circle-outline': Send,
'mdi-tag-outline': Tag,
'mdi-upload': Upload,
};
const liderraLucideSet: IconSet = {
component: (props: IconProps) => {
const Icon = lucideMap[String(props.icon)] || HelpCircle;
return h(Icon, { size: 20, strokeWidth: 1.75 });
},
};
export const vuetify = createVuetify({
// Русская локаль Vuetify — иначе дефолтные строки таблиц/пагинации на
// английском («No data available», «Items per page», «0-0 of 0») на всех экранах.
locale: {
locale: 'ru',
fallback: 'en',
messages: { ru },
},
theme: {
defaultTheme: 'liderraForest',
themes: { liderraForest },
},
icons: {
defaultSet: 'liderra',
sets: { liderra: liderraLucideSet },
},
defaults: {
VBtn: {
variant: 'flat',
rounded: 'lg',
},
VCard: {
rounded: 'lg',
variant: 'flat',
border: true,
},
VTextField: {
variant: 'outlined',
density: 'comfortable',
color: 'primary',
},
VTextarea: {
variant: 'outlined',
density: 'comfortable',
color: 'primary',
},
VSelect: {
variant: 'outlined',
density: 'comfortable',
},
VChip: {
rounded: 'pill',
size: 'small',
},
VDataTable: {
density: 'comfortable',
},
VDialog: {
scrim: 'rgba(1, 32, 25, 0.32)',
},
},
});