# SparkAntiplagiat — Конвенции и правила разработки > Документ описывает архитектурные, стилевые и технические решения фронтенда SparkAntiplagiat. > Базируется на конвенциях родственного проекта SparkGuardian и наследует общие правила (Angular 21, signals, Taiga UI, T-Bank tokens). > **Передавайте этот файл AI-ассистенту вместе с задачей** — описанные правила должны соблюдаться при любых изменениях. --- ## 1. Технологический стек | Область | Решение | Версия | |---------|---------|--------| | Фреймворк | Angular | 21+ | | Язык | TypeScript | strict mode | | UI-библиотека | Taiga UI | v5 | | Формы / маски | Maskito | 5.x | | Web API wrappers | @ng-web-apis | 5.x | | Стили | Vanilla CSS + CSS Custom Properties | — | | Линтинг | ESLint + angular-eslint | — | | Тесты | Vitest + jsdom | — | | Анимации | `@angular/animations` | — | | Шрифт | Tinkoff Sans (фирменный; системный fallback: `system-ui, sans-serif`) | — | **Категорически НЕ используется:** TailwindCSS, SCSS/SASS/LESS, NgModules, RxJS-only подход к состоянию (signals предпочтительны). --- ## 2. Архитектура: слоевая модель Проект строго разделён на три слоя. Ни один вышестоящий слой не импортирует из нижестоящего (feature не экспортирует наружу, core не импортирует feature). ``` src/app/ ├── core/ ← синглтоны, DI-токены, interceptors, сервисы, модели, pipes │ ├── config/ ← InjectionToken'ы (API_BASE_URL, API_ORIGIN, DEFAULT_PAGE_LIMIT) │ ├── devtools/ ← DevLogService (ring-buffer 300 записей) │ ├── guards/ ← authGuard │ ├── http/ ← interceptors (apiBaseUrl, auth, devLog), error-classification, http-error utils │ ├── models/ ← api.types.ts — ВСЕ TypeScript-интерфейсы API │ ├── notifications/ ← UserErrorNotifyService, user-error-messages config │ ├── services/ ← доменные API-клиенты: WorksApi, AnalysisRunsApi, │ │ StudentsApi, GroupsApi, EventsApi, ReferenceSetsApi, │ │ UsersApi, AuditApi + AuthService │ ├── monitoring/ ← pipes для audit log │ └── works/ ← pipes: analysis-run-status, chip-classes │ ├── features/ ← lazy-loaded smart-компоненты (по одному на маршрут) │ ├── landing/ ← LandingComponent (маркетинговая страница) │ ├── login/ ← LoginComponent │ ├── dashboard/ ← DashboardComponent (сводка доменов + CRUD-формы) │ ├── works/ ← WorksList, WorkDetail (загрузка архива, запуск check, │ │ отчёты, adoptions по прогонам) │ ├── groups/ ← GroupDetail (участники, привязка студентов/юзеров) │ ├── students/ ← StudentDetail │ ├── events/ ← EventDetail (CRUD + works события) │ ├── reference-sets/ ← RefsetDetail (ingestions, CRUD) │ ├── monitoring/ ← MonitoringComponent (audit logs + фильтры) │ └── devtools/ ← DevConsoleComponent (overlay, только isDevMode()) │ └── shared/ ← чистые pure-функции, НИКАКИХ Angular-зависимостей └── utils/ ← date-time, duration, json, math, number ``` ### Правила файловой организации - **Один компонент = одна директория**: `feature-name/feature-name.component.ts`, `feature-name.html`, `feature-name.css`. - **Имена файлов**: исключительно `kebab-case`. - **Селекторы компонентов**: `app-` (e.g., `app-session-detail`). - **Классы компонентов**: `PascalCase` + суффикс `Component` (e.g., `SessionDetailComponent`). - **Pipes**: суффикс `Pipe` (e.g., `SessionStatusPipe`). - **Сервисы**: суффикс `Service` (e.g., `WorksApiService`). - **Утилиты**: суффикс `.util.ts` (e.g., `date-time.util.ts`). - **Конфиги**: суффикс `.config.ts` (e.g., `user-error-messages.config.ts`). - **Типы**: суффикс `.types.ts` (e.g., `api.types.ts`). --- ## 3. Angular-паттерны ### 3.1 Компоненты Каждый компонент **обязан** соответствовать: ```typescript @Component({ selector: 'app-my-component', imports: [/* только то, что используется в шаблоне */], templateUrl: './my-component.html', // отдельный файл, НЕ inline template styleUrl: './my-component.css', // singular (Angular 21 syntax), НЕ styleUrls changeDetection: ChangeDetectionStrategy.OnPush, // ОБЯЗАТЕЛЬНО }) export class MyComponent { // DI через inject(), НЕ constructor injection private readonly api = inject(WorksApiService); private readonly userErrors = inject(UserErrorNotifyService); // Inputs/Outputs через signal-based API (Angular 17+) readonly sessionId = input.required(); readonly excludeMouseMoves = model(true); readonly telemetryToMsChange = output(); // Локальный state через signal() protected readonly isLoading = signal(false); // Вычисляемые значения через computed() protected readonly totalPages = computed(() => Math.ceil(this.total() / this.limit), ); } ``` **Запрещено:** - `@Input()` / `@Output()` декораторы — используй `input()` / `output()` / `model()`. - `ChangeDetectionStrategy.Default` — всегда `OnPush`. - constructor injection — всегда `inject()`. - `styleUrls: [...]` (plural) — всегда `styleUrl: '...'` (singular, Angular 21+). - Inline `template:` и `styles:` (кроме крошечных компонентов < 15 строк шаблона). ### 3.2 Маршрутизация Все feature-компоненты загружаются **лениво** через `loadComponent`: ```typescript export const routes: Routes = [ { path: 'sessions', loadComponent: () => import('./features/sessions/sessions-list/sessions-list.component') .then((m) => m.SessionsListComponent), }, { path: '**', redirectTo: '' }, ]; ``` ### 3.3 Тяжёлые вложенные компоненты: `@defer` Если одновременно видна только одна вкладка/секция — оборачивай в `@defer`: ```html @case (1) { @defer { } @placeholder {
} } ``` ### 3.4 Signals vs RxJS | Задача | Инструмент | |--------|-----------| | Локальный UI state (boolean, number, string) | `signal()` | | Derived/вычисляемые данные | `computed()` | | Two-way binding | `model()` | | HTTP-запросы | RxJS `Observable` → `async` pipe | | Подписки в компоненте | `toObservable()` + `switchMap/combineLatest` | | Lifecycle-cleanup | `effect(onCleanup)` | **Паттерн загрузки данных** — единая цепочка с тремя статусами: ```typescript protected readonly data$ = toObservable(this.sessionId).pipe( switchMap((id) => this.api.getData(id).pipe( map((data) => ({ status: 'ok' as const, data })), catchError((e: HttpErrorResponse) => { this.userErrors.notifyError(e, 'Контекст ошибки'); return of({ status: 'error' as const }); }), startWith({ status: 'loading' as const }), ), ), ); ``` В шаблоне: ```html @if (data$ | async; as state) { @switch (state.status) { @case ('loading') { } @case ('error') {

Не удалось загрузить данные.

} @case ('ok') { /* контент --> } } } ``` ### 3.5 DI-токены Конфигурации выносятся в `InjectionToken` с `factory`, а НЕ читаются напрямую из `environment`: ```typescript // core/config/api.tokens.ts export const API_BASE_URL = new InjectionToken('API_BASE_URL', { factory: () => { const doc = inject(DOCUMENT); const loc = doc.defaultView?.location; if (loc?.protocol === 'file:') { return `${environment.apiFallbackOrigin}${environment.apiBasePath}`; } return environment.apiBasePath; }, }); ``` Это обеспечивает тестируемость (можно подменить токен без мока `environment`) и поддержку `file://` протокола. ### 3.6 Interceptors Два функциональных interceptor'а (Angular 21 style): 1. **`apiBaseUrlInterceptor`** — добавляет `API_BASE_URL` ко всем запросам, кроме: - абсолютных URL (`https://...`) - статических ассетов (`/svg/`, `/fonts/`, `/images/`) 2. **`devLogInterceptor`** — логирует **все** HTTP-запросы в `DevLogService` (только `isDevMode()`). Регистрация в `app.config.ts`: ```typescript provideHttpClient(withInterceptors([apiBaseUrlInterceptor, devLogInterceptor])), ``` ### 3.7 Обработка ошибок Полный pipeline: ``` HttpErrorResponse └→ classifyUserError(err): UserErrorKind └→ friendlyMessageForUserError(kind): string для пользователя └→ UserErrorNotifyService.notifyError() → TuiNotificationService ``` Классификация по HTTP-кодам и паттернам: | Код/паттерн | UserErrorKind | |-------------|---------------| | `status === 0` | `network` | | `status === 401` | `unauthorized` | | `status === 403` | `forbidden` | | `status === 404` | `not_found` | | `status >= 500` | `server_error` | | `TimeoutError` | `timeout` | | `'Http failure during parsing'` | `parse_error` | Дружелюбные сообщения определяются в `user-error-messages.config.ts`. ### 3.8 Pipes Standalone pipes для трансформации данных в шаблонах: ```typescript @Pipe({ name: 'sessionStatus', standalone: true, }) export class SessionStatusPipe implements PipeTransform { private readonly devLog = inject(DevLogService); transform(value: string | null | undefined): string { // ... маппинг + warn в DevLog для неизвестных значений } } ``` Pipes **логируют** неизвестные значения в `DevLogService` (только devMode), чтобы разработчик заметил незнакомые статусы/типы. --- ## 4. Дизайн-система и CSS ### 4.1 Принцип: CSS Custom Properties + Taiga UI overrides Все цвета, отступы и параметры анимаций **централизованы** в глобальном файле `src/styles/color-tokens.css`. ```css :root { /* SparkGuardian проектные токены */ --sg-color-accent: #ffdb00; --sg-color-bg: #f6f7f8; --sg-color-text: #383839; --sg-color-subtitle: #313132; --sg-color-form-bg: #e8edf1; --sg-color-placeholder: #6b6d6f; /* Семантические алиасы */ --sg-color-card-bg: #ffffff; --sg-color-border: color-mix(in srgb, var(--sg-color-text) 12%, transparent); --sg-color-danger: #d92d20; /* Layout */ --sg-content-max-width: 1104px; --sg-page-padding-inline: 1rem; /* Taiga UI overrides – переопределяем цвета дизайн-системы */ --tui-background-accent-1: var(--sg-color-accent); --tui-background-base: var(--sg-color-bg); --tui-background-elevation-1: var(--sg-color-card-bg); --tui-text-primary: var(--sg-color-text); --tui-border-normal: var(--sg-color-border); --tui-focus: var(--sg-color-accent); } ``` ### 4.2 Naming convention для токенов | Prefix | Назначение | Пример | |--------|-----------|--------| | `--sg-color-*` | Базовые цвета проекта | `--sg-color-accent` | | `--sg-filter-chip-*` | Чипы фильтров | `--sg-filter-chip-active-bg` | | `--sg-session-status-*` | Раскраска статусов сессий | `--sg-session-status-active-fg` | | `--sg-keyboard-*` | Визуализация клавиатуры | `--sg-keyboard-key-pressed-fill` | | `--sg-input-highlight-*` | Анимации подсветки | `--sg-input-highlight-duration` | | `--sg-textfield-*` | Текстовые поля | `--sg-textfield-radius` | | `--sg-native-input-*` | Нативные `` | `--sg-native-input-min-height` | | `--tui-*` | Переопределения Taiga UI | `--tui-background-accent-1` | ### 4.3 Правила работы со стилями 1. **Никогда не используй захардкоженных цветов** в компонентных CSS. Всё через `var(--sg-*)` или `var(--tui-*)`. 2. **Не дублируй глобальные классы** в компонентных стилях. Глобальные `.muted`, `.small`, `.mono`, `.section-title`, `.card`, `.loading-wrap` определены в `page-common.css` и доступны везде. 3. **`color-mix()`** — основной инструмент для производных цветов (hover, faded, border). 4. **Кнопки Taiga UI кастомизируются** через CSS-селекторы с `[tuiButton][tuiAppearance][data-appearance='secondary']`. 5. **Текстовые поля** стилизуются через два паттерна: - Taiga `tui-textfield` → класс `.sg-tui-textfield` - Нативный `` → класс `.sg-native-input` 6. **Focus-visible vs focus** — при клике снимаем outline/box-shadow; ring только для keyboard navigation. ### 4.4 Глобальные CSS-файлы | Файл | Назначение | |------|-----------| | `color-tokens.css` | Все CSS-переменные и Taiga overrides | | `page-common.css` | `.card`, `.section-title`, `.loading-wrap`, `.muted`, `.small`, `.mono`, `.page` | | `sg-input-fields.css` | Стилизация `tui-textfield` и нативных `` | | `filter-chips.css` | Стилизация toggle-кнопок / чипов фильтрации | | `session-status-chips.css` | Раскраска `[tuiChip]` по статусам сессии | ### 4.5 Layout ```css /* Используется для ограничения ширины контента */ .sg-content-column { max-width: var(--sg-content-max-width); /* 1104px */ margin-inline: auto; padding-inline: var(--sg-page-padding-inline); /* 1rem / 48px на мобильных */ } ``` Все page-уровневые контейнеры добавляют класс `.page .sg-content-column`. ### 4.6 Шрифт Шрифт Tinkoff Sans подключается глобально. В CSS используется `font-family: 'Tinkoff Sans', system-ui, sans-serif`. Для кода/моноширинных блоков: `font-family: ui-monospace, monospace`. --- ## 5. Taiga UI: правила интеграции ### 5.1 Инициализация ```typescript // app.config.ts providers: [ provideAnimations(), provideTaiga(), tuiNotificationOptionsProvider(() => ({ block: 'start', inline: 'end', })), ] ``` Корневой шаблон оборачивается в ``: ```html
...
@if (isDev) { }
``` ### 5.2 Используемые компоненты Taiga UI | Компонент | Импорт | Назначение | |-----------|--------|-----------| | `TuiButton` | `@taiga-ui/core/components/button` | Primary/secondary кнопки | | `TuiLoader` | `@taiga-ui/core/components/loader` | Индикаторы загрузки | | `TuiLink` | `@taiga-ui/core/components/link` | Навигационные ссылки | | `TuiTitle` | `@taiga-ui/core/components/title` | Заголовки с семантикой | | `TuiTabs` | `@taiga-ui/core/directives/tabs` | Вкладки | | `TuiIcon` | `@taiga-ui/core/components/icon` | Иконки | | `TuiInput` | `@taiga-ui/core/components/input` | Текстовые поля (spread: `...TuiInput`) | | `TuiChip` | `@taiga-ui/kit/components/chip` | Чипы/бейджи | | `TuiPagination` | `@taiga-ui/kit/components/pagination` | Пагинация | | `TuiAccordion` | `@taiga-ui/kit` | Аккордеоны | | `TuiNotificationService` | `@taiga-ui/core/components/notification` | Всплывающие уведомления | ### 5.3 Кастомизация Taiga-компонентов **Не меняй глобальные стили Taiga** напрямую. Вместо этого: 1. Переопредели `--tui-*` переменные в `color-tokens.css`. 2. Используй CSS-селекторы с `[tuiAppearance][data-appearance='...']` для точечных кастомизаций. 3. Добавляй собственные CSS-классы (`.sg-tui-textfield`, `.stream-active`) для стилевых модификаторов. --- ## 6. TypeScript: строгость и конвенции ### 6.1 tsconfig ```json { "compilerOptions": { "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "isolatedModules": true, "target": "ES2022", "module": "preserve" }, "angularCompilerOptions": { "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true } } ``` ### 6.2 ESLint ключевые правила - `no-console: ['warn', { allow: ['warn', 'error'] }]` — нет `console.log` в продакшне. - `no-alert: 'error'`, `no-debugger: 'error'` — безопасность. - Компоненты: prefix `app-`, kebab-case. - Директивы: prefix `app`, camelCase. - `@angular-eslint/template/no-call-expression: 'off'` — мы вызываем методы в шаблонах (computed → formatDate и т.д.). ### 6.3 Правила кода - **`@ts-ignore` запрещён**. Используй `keyof`, `Record`, type assertions. - **Неиспользуемые импорты** удаляются немедленно. - **Falsy checks** (`!value`) — НЕ использовать для числовых значений, которые могут быть `0`. Используй `value == null`. - **Track expressions** в `@for`: использовать стабильные ключи (`track item.id` или `track item.timestamp + '_' + item.type`), а НЕ `track $index`. - **Pure utility функции** (без Angular-зависимости) → `shared/utils/`. Никогда не дублировать: вынести в `shared`. --- ## 7. Паттерны проектирования ### 7.1 Rule-based Engine (расширяемая обработка) Вместо switch/case используется массив правил: ```typescript // types export interface TelemetrySummaryRule { readonly id: string; readonly match: (o: Record) => boolean; readonly summarize: (o: Record) => string; } // config — просто массив, легко расширить добавлением одного элемента export const TELEMETRY_SUMMARY_RULES: readonly TelemetrySummaryRule[] = [ mouseClickRule, mouseMoveRule, keyboardKeyRule, ]; // engine — итерирует и применяет первое совпавшее правило export function summarizeTelemetryData(data: unknown): string { const raw = unwrapJsonPayload(data); const o = raw as Record; for (const rule of TELEMETRY_SUMMARY_RULES) { if (rule.match(o)) { return rule.summarize(o); } } return fallbackCompactJson(o); } ``` ### 7.2 Classification Pipeline (обработка ошибок) ``` Raw Error → classify(err) → UserErrorKind → friendlyMessage(kind) → UI Notification ``` Разделяет три ответственности: 1. **Классификатор** (`error-classification.util.ts`) — чистая функция, тестируемая. 2. **Словарь сообщений** (`user-error-messages.config.ts`) — `Record`. 3. **Сервис уведомления** (`UserErrorNotifyService`) — injectable, использует Taiga UI. ### 7.3 DevTools с ring-buffer ```typescript @Injectable({ providedIn: 'root' }) export class DevLogService { private seq = 0; private readonly max = 300; readonly entries = signal([]); add(entry: Omit): void { const withMeta: DevLogEntry = { id: ++this.seq, time: new Date().toISOString(), ...entry, }; this.entries.update((curr) => [...curr.slice(-(this.max - 1)), withMeta]); } } ``` Всё логирование за `isDevMode()` → нулевой overhead в продакшне. ### 7.4 SVG-инъекция (работа с графикой) Для динамической подсветки элементов SVG: 1. Загружаем SVG через `HttpClient` (responseType: `text`). 2. Кэшируем через `shareReplay({ bufferSize: 1, refCount: false })`. 3. Инжектируем `