backup: SCSS before reorganization - create backup folder and plan

This commit is contained in:
Игорь Надеин 2026-01-15 14:03:27 +03:00
parent f44a9ac23d
commit e13ddb9a8b
50 changed files with 13832 additions and 0 deletions

View File

@ -0,0 +1,197 @@
// =============================================================================
// CSS Custom Properties (CSS Variables)
// =============================================================================
// CSS переменные, доступные в runtime через var(--variable-name)
// В отличие от SCSS переменных ($variable), эти работают в браузере
:root {
// ========== Цветовая схема ==========
// Основные цвета приложения
--app-primary: #0d6efd;
--app-success: #198754;
--app-danger: #dc3545;
--app-warning: #ffc107;
--app-info: #0dcaf0;
// Фоновые цвета
--bg-light: #f8f9fa;
--bg-dark: #212529;
--bg-card: #ffffff;
// Цвета текста
--text-primary: #212529;
--text-secondary: #6c757d;
--text-muted: #adb5bd;
// ========== Размеры и отступы ==========
// Высота компонентов
--navbar-h: 60px;
--sidebar-w: 200px;
--rightbar-w: 350px;
// Радиусы скругления
--radius-xs: 4px;
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 14px;
--radius-xl: 18px;
--radius-2xl: 24px;
--radius-full: 50%;
// Радиусы компонентов
--feed-radius: var(--radius-xl);
--card-radius: var(--radius-lg);
--button-radius: var(--radius-md);
--input-radius: var(--radius-md);
--badge-radius: 999px;
// ========== Унифицированная система отступов (8px grid) ==========
// Базовая шкала отступов
--space-0: 0;
--space-1: 0.25rem; // 4px
--space-2: 0.5rem; // 8px
--space-3: 0.75rem; // 12px
--space-4: 1rem; // 16px
--space-5: 1.5rem; // 24px
// Семантические названия (для лучшей читаемости)
--space-xs: var(--space-1);
--space-sm: var(--space-2);
--space-md: var(--space-3);
--space-lg: var(--space-4);
--space-xl: var(--space-5);
// Специфичные отступы для компонентов
--gap-component: var(--space-3);
--gap-section: var(--space-4);
// Padding карточек и контейнеров
--card-padding: var(--space-2);
--card-padding-sm: var(--space-1);
--card-padding-lg: var(--space-3);
// Margin между элементами
--margin-section: var(--space-3);
--margin-block: var(--space-4);
// Layout отступы (app-layout grid)
--layout-gap: var(--space-2);
--layout-padding-y: var(--space-3);
--layout-padding-x: 0;
// Старые переменные (deprecated, для обратной совместимости)
--spacing-xs: var(--space-1);
--spacing-sm: var(--space-2);
--spacing-md: var(--space-3);
--spacing-lg: var(--space-4);
--spacing-xl: var(--space-5);
// ========== Тени ==========
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08);
--shadow-md: 0 2px 8px rgba(0, 0, 0, 0.08);
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.12);
--shadow-xl: 0 8px 32px rgba(0, 0, 0, 0.16);
// Тени компонентов
--feed-shadow: var(--shadow-md);
--feed-shadow-hover: var(--shadow-lg);
--card-shadow: var(--shadow-sm);
--dropdown-shadow: var(--shadow-lg);
--modal-shadow: var(--shadow-xl);
// ========== Компоненты Feed ==========
--feed-gap: 0.75rem;
// ========== iOS Компоненты ==========
--ios-overlay-bg: rgba(0, 0, 0, 0.4);
--ios-sheet-radius: 24px 24px 0 0;
--ios-grip-w: 36px;
--ios-grip-h: 5px;
--ios-search-radius: var(--radius-md);
// ========== Аватары ==========
--avatar-xs: 28px;
--avatar-sm: 36px;
--avatar-md: 44px;
--avatar-lg: 64px;
--avatar-xl: 96px;
// ========== Календарь ==========
--calendar-day-size: 1.6rem;
--calendar-event-radius: 6px;
--calendar-wrap-radius: 10px;
// ========== Переходы и анимации ==========
--transition-fast: 0.15s ease;
--transition-base: 0.2s ease;
--transition-slow: 0.3s ease;
// ========== Z-индексы ==========
--z-dropdown: 1000;
--z-sticky: 1020;
--z-fixed: 1030;
--z-modal-backdrop: 1040;
--z-modal: 1050;
--z-popover: 1060;
--z-tooltip: 1070;
// ========== Типографика ==========
--font-family-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-family-mono: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--font-size-xs: 0.75rem;
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--font-size-lg: 1.125rem;
--font-size-xl: 1.25rem;
--font-weight-light: 300;
--font-weight-normal: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
// ========== Границы ==========
--border-width: 1px;
--border-color: #dee2e6;
--border-radius: var(--radius-md);
}
// ========== Вспомогательные классы с переменными ==========
.shadow-sm { box-shadow: var(--shadow-sm); }
.shadow-md { box-shadow: var(--shadow-md); }
.shadow-lg { box-shadow: var(--shadow-lg); }
.shadow-xl { box-shadow: var(--shadow-xl); }
.radius-sm { border-radius: var(--radius-sm); }
.radius-md { border-radius: var(--radius-md); }
.radius-lg { border-radius: var(--radius-lg); }
.radius-xl { border-radius: var(--radius-xl); }
.radius-full { border-radius: var(--radius-full); }
.transition-fast { transition: all var(--transition-fast); }
.transition-base { transition: all var(--transition-base); }
.transition-slow { transition: all var(--transition-slow); }
// ========== Адаптивные переменные ==========
// На мобильных navbar выше (90px вместо 60px)
@media (max-width: 767.98px) {
:root {
--navbar-h: 90px;
}
}

View File

@ -0,0 +1,8 @@
// =============================================================================
// Index - Centralized Imports
// =============================================================================
// Единая точка импорта всех abstracts компонентов
@import "variables";
@import "css-variables";
@import "mixins";

View File

@ -0,0 +1,378 @@
// =============================================================================
// Mixins - Reusable Style Patterns
// =============================================================================
// Переиспользуемые паттерны для DRY (Don't Repeat Yourself) подхода
// =============================================================================
// 1. GLASSMORPHISM EFFECTS
// =============================================================================
/// Применяет эффект glassmorphism (размытое стекло)
/// @param {string} $blur - Уровень размытия (по умолчанию $blur-md)
/// @param {color} $bg - Цвет фона с прозрачностью
/// @param {color} $border - Цвет границы (опционально)
@mixin glassmorphism($blur: $blur-md, $bg: $glass-light-bg, $border: null) {
backdrop-filter: $blur saturate(180%);
-webkit-backdrop-filter: $blur saturate(180%);
background: $bg;
@if $border {
border: 1px solid $border;
}
}
/// Glassmorphism для светлой темы
@mixin glassmorphism-light() {
@include glassmorphism($blur-md, $glass-light-bg, $glass-light-border);
}
/// Glassmorphism для тёмной темы
@mixin glassmorphism-dark() {
@include glassmorphism($blur-md, $glass-dark-bg, $glass-dark-border);
}
/// Glassmorphism для карточек
@mixin glassmorphism-card() {
@include glassmorphism($blur-md, $card-glass-bg, $card-glass-border);
box-shadow: $card-glass-shadow;
}
// =============================================================================
// 2. SHADOWS
// =============================================================================
/// Применяет тень с возможностью hover эффекта
/// @param {string} $shadow - Базовая тень
/// @param {string} $shadow-hover - Тень при наведении (опционально)
@mixin shadow($shadow: $shadow-md, $shadow-hover: null) {
box-shadow: $shadow;
@if $shadow-hover {
transition: box-shadow $transition-base;
&:hover {
box-shadow: $shadow-hover;
}
}
}
/// Удаляет все тени
@mixin no-shadow() {
box-shadow: none !important;
}
// =============================================================================
// 3. BORDER RADIUS
// =============================================================================
/// Применяет border-radius с единым значением
/// @param {string} $radius - Размер радиуса
@mixin radius($radius: $radius-lg) {
border-radius: $radius;
}
/// Скругляет только верхние углы
@mixin radius-top($radius: $radius-lg) {
border-top-left-radius: $radius;
border-top-right-radius: $radius;
}
/// Скругляет только нижние углы
@mixin radius-bottom($radius: $radius-lg) {
border-bottom-left-radius: $radius;
border-bottom-right-radius: $radius;
}
/// Создаёт идеальный круг
@mixin circle() {
border-radius: 50%;
}
/// Создаёт pill-образную форму (капсула)
@mixin pill() {
border-radius: $radius-pill;
}
// =============================================================================
// 4. FLEXBOX UTILITIES
// =============================================================================
/// Центрирует содержимое по обеим осям
@mixin flex-center() {
display: flex;
align-items: center;
justify-content: center;
}
/// Flexbox с вертикальным центрированием
@mixin flex-align-center() {
display: flex;
align-items: center;
}
/// Flexbox с горизонтальным центрированием
@mixin flex-justify-center() {
display: flex;
justify-content: center;
}
/// Flexbox со space-between
@mixin flex-between() {
display: flex;
align-items: center;
justify-content: space-between;
}
// =============================================================================
// 5. POSITION UTILITIES
// =============================================================================
/// Абсолютное позиционирование по центру
@mixin absolute-center() {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
/// Абсолютное позиционирование с полным покрытием
@mixin absolute-cover() {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
/// Sticky positioning с поддержкой navbar offset
/// @param {string} $top - Отступ сверху
@mixin sticky($top: 0) {
position: sticky;
top: $top;
z-index: $z-sticky;
}
// =============================================================================
// 6. TRANSITIONS
// =============================================================================
/// Применяет transition к указанным свойствам
/// @param {list} $properties - Список свойств для анимации
/// @param {string} $duration - Длительность (по умолчанию $transition-base)
@mixin transition($properties, $duration: $transition-base) {
transition-property: $properties;
transition-duration: $duration;
transition-timing-function: ease;
}
/// Плавная анимация для всех свойств
@mixin transition-all($duration: $transition-base) {
transition: all $duration;
}
// =============================================================================
// 7. SCROLLBAR STYLING
// =============================================================================
/// Кастомный scrollbar
/// @param {color} $track-bg - Цвет трека
/// @param {color} $thumb-bg - Цвет ползунка
/// @param {string} $width - Ширина scrollbar
@mixin custom-scrollbar($track-bg: rgba(0, 0, 0, 0.05), $thumb-bg: rgba(0, 0, 0, 0.3), $width: 6px) {
scrollbar-width: thin;
scrollbar-color: $thumb-bg $track-bg;
&::-webkit-scrollbar {
width: $width;
height: $width;
}
&::-webkit-scrollbar-track {
background: $track-bg;
border-radius: $radius-sm;
}
&::-webkit-scrollbar-thumb {
background: $thumb-bg;
border-radius: $radius-sm;
&:hover {
background: rgba($thumb-bg, 0.8);
}
}
}
// =============================================================================
// 8. TEXT UTILITIES
// =============================================================================
/// Обрезает текст с многоточием
@mixin text-truncate() {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/// Ограничивает текст указанным количеством строк
/// @param {number} $lines - Количество строк
@mixin text-clamp($lines: 2) {
display: -webkit-box;
-webkit-line-clamp: $lines;
-webkit-box-orient: vertical;
overflow: hidden;
}
// =============================================================================
// 9. HOVER EFFECTS
// =============================================================================
/// Стандартный hover эффект с opacity
/// @param {number} $opacity - Прозрачность при наведении
@mixin hover-opacity($opacity: 0.8) {
transition: opacity $transition-fast;
cursor: pointer;
&:hover {
opacity: $opacity;
}
}
/// Hover с подсветкой фона
/// @param {color} $bg - Цвет фона при наведении
@mixin hover-bg($bg: $gray-50) {
transition: background-color $transition-fast;
cursor: pointer;
&:hover {
background-color: $bg;
}
}
/// Hover с transform scale
/// @param {number} $scale - Масштаб при наведении
@mixin hover-scale($scale: 1.05) {
transition: transform $transition-base $ease-smooth;
cursor: pointer;
&:hover {
transform: scale($scale);
}
}
// =============================================================================
// 10. BADGE UTILITIES
// =============================================================================
/// Создаёт базовый Telegram-style badge
/// @param {string} $height - Высота badge
/// @param {string} $radius - Border radius
@mixin badge-base($height: $badge-height-base, $radius: $badge-radius) {
@include flex-center();
height: $height;
min-width: $height;
border-radius: $radius;
font-size: $badge-font-size;
font-weight: $badge-font-weight;
line-height: 1;
}
/// Badge с тенью
@mixin badge-shadow() {
box-shadow: $badge-shadow;
&:hover {
box-shadow: $badge-shadow-hover;
}
}
// =============================================================================
// 11. RESPONSIVE UTILITIES
// =============================================================================
/// Media query для mobile устройств
@mixin mobile() {
@media (max-width: 767.98px) {
@content;
}
}
/// Media query для tablet устройств
@mixin tablet() {
@media (min-width: 768px) and (max-width: 991.98px) {
@content;
}
}
/// Media query для desktop устройств
@mixin desktop() {
@media (min-width: 992px) {
@content;
}
}
/// Media query для больших экранов
@mixin large-desktop() {
@media (min-width: 1200px) {
@content;
}
}
// =============================================================================
// 12. DARK MODE UTILITIES
// =============================================================================
/// Применяет стили для тёмной темы
@mixin dark-mode() {
[data-bs-theme="dark"] & {
@content;
}
}
/// Применяет стили для светлой темы
@mixin light-mode() {
[data-bs-theme="light"] & {
@content;
}
}
// =============================================================================
// 13. ANIMATION UTILITIES
// =============================================================================
/// Fade-in анимация
@mixin fade-in($duration: $transition-base) {
animation: fadeIn $duration ease-in;
}
/// Slide-down анимация
@mixin slide-down($duration: $transition-base) {
animation: slideDown $duration $ease-smooth;
}
/// Pulse анимация
@mixin pulse($duration: 2s) {
animation: pulse $duration infinite;
}
// =============================================================================
// 14. FOCUS STATES
// =============================================================================
/// Bootstrap-style focus ring
/// @param {color} $color - Цвет focus ring
@mixin focus-ring($color: var(--bs-primary-rgb)) {
&:focus,
&:focus-visible {
outline: 0;
box-shadow: 0 0 0 0.2rem rgba($color, 0.25);
}
}
/// Убирает focus outline
@mixin no-focus() {
&:focus,
&:focus-visible {
outline: none;
box-shadow: none;
}
}

View File

@ -0,0 +1,223 @@
// =============================================================================
// Design Tokens - Centralized Variables
// =============================================================================
// Централизованная система переменных для всего проекта
// Все компоненты должны использовать эти токены вместо hardcoded значений
// =============================================================================
// 1. ЦВЕТОВАЯ СИСТЕМА
// =============================================================================
// Telegram colors
$telegram-blue: #0088cc;
$telegram-mention-bg: #0088cc;
// Gradient colors
$gradient-purple-start: #667eea;
$gradient-purple-end: #764ba2;
$gradient-pink-start: #f093fb;
$gradient-pink-end: #f5576c;
// Base colors
$color-white: #fff;
$color-black: #000;
// Gray scale
$gray-50: #f8f9fa;
$gray-100: #f0f0f0;
$gray-200: #e3e3e3;
$gray-300: #dee2e6;
$gray-400: #ced4da;
$gray-500: #adb5bd;
$gray-600: #6c757d;
$gray-700: #495057;
$gray-800: #343a40;
$gray-900: #212529;
// Semantic colors
$color-danger: #ef4444;
$color-success-light: #e3f2fd;
$color-warning-light: #fffbeb;
// =============================================================================
// 2. SPACING SYSTEM (уже определена в custom-bootstrap.scss)
// =============================================================================
// Используем Bootstrap переменные $spacers
// 0: 0, 1: 4px, 2: 8px, 3: 12px, 4: 16px, 5: 24px
// =============================================================================
// 3. BORDER RADIUS TOKENS
// =============================================================================
$radius-xs: 4px; // extra small
$radius-sm: 6px; // small
$radius-md: 10px; // medium (most common)
$radius-lg: 12px; // large
$radius-xl: 14px; // extra large
$radius-2xl: 16px; // 2x large
$radius-3xl: 18px; // 3x large
$radius-4xl: 20px; // 4x large
$radius-pill: 999px; // perfect pill shape
// Badge specific (perfect circles)
$badge-radius: 10px;
$badge-radius-sm: 9px;
$badge-radius-lg: 12px;
$badge-radius-xl: 14px;
// =============================================================================
// 4. SHADOW TOKENS
// =============================================================================
$shadow-none: none;
$shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.06);
$shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1);
$shadow-md: 0 2px 4px rgba(0, 0, 0, 0.1);
$shadow-lg: 0 2px 8px rgba(0, 0, 0, 0.15);
$shadow-xl: 0 4px 12px rgba(0, 0, 0, 0.15);
$shadow-2xl: 0 4px 16px rgba(0, 0, 0, 0.2);
$shadow-3xl: 0 6px 20px rgba(0, 0, 0, 0.3);
// Badge specific shadows
$badge-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
$badge-shadow-hover: 0 3px 6px rgba(0, 0, 0, 0.2);
$badge-shadow-dark: 0 2px 4px rgba(0, 0, 0, 0.3);
// Context menu shadows
$context-menu-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
// =============================================================================
// 5. BLUR TOKENS (для backdrop-filter)
// =============================================================================
$blur-none: blur(0);
$blur-sm: blur(8px);
$blur-md: blur(10px);
$blur-lg: blur(12px);
$blur-xl: blur(16px);
// =============================================================================
// 6. OPACITY TOKENS
// =============================================================================
$opacity-glass-light: 0.1;
$opacity-glass-medium: 0.15;
$opacity-glass-high: 0.2;
$opacity-overlay-light: 0.05;
$opacity-overlay-medium: 0.1;
$opacity-overlay-dark: 0.3;
$opacity-border-light: 0.08;
$opacity-border-medium: 0.1;
$opacity-border-dark: 0.25;
// =============================================================================
// 7. TRANSITION TOKENS
// =============================================================================
$transition-fast: 0.15s ease;
$transition-base: 0.2s ease;
$transition-medium: 0.3s ease;
$transition-slow: 0.4s ease;
// Специальные easing функции
$ease-bounce: cubic-bezier(0.175, 0.885, 0.32, 1.275);
$ease-smooth: cubic-bezier(0.4, 0, 0.2, 1);
// Badge specific
$badge-transition: transform 0.2s $ease-bounce;
// =============================================================================
// 8. Z-INDEX TOKENS
// =============================================================================
$z-base: 1;
$z-dropdown: 1000;
$z-sticky: 1020;
$z-fixed: 1030;
$z-modal-backdrop: 1040;
$z-modal: 1050;
$z-popover: 1060;
$z-tooltip: 1070;
$z-notification: 1080;
// =============================================================================
// 9. SIZE TOKENS (avatars, badges, icons)
// =============================================================================
// Badge heights
$badge-height-sm: 18px;
$badge-height-base: 20px;
$badge-height-lg: 24px;
$badge-height-xl: 28px;
// Badge padding
$badge-padding-x: 7px;
$badge-padding-y: 2px;
// Avatar sizes
$avatar-xs: 24px;
$avatar-sm: 32px;
$avatar-md: 40px;
$avatar-lg: 44px;
$avatar-xl: 48px;
$avatar-2xl: 56px;
// Icon button sizes
$btn-icon-sm: 32px;
$btn-icon-base: 36px;
$btn-icon-lg: 44px;
// =============================================================================
// 10. GLASSMORPHISM PRESETS
// =============================================================================
// Light theme glassmorphism
$glass-light-bg: rgba(255, 255, 255, 0.1);
$glass-light-border: rgba(0, 0, 0, 0.08);
// Dark theme glassmorphism
$glass-dark-bg: rgba(52, 58, 64, 0.95);
$glass-dark-border: rgba(255, 255, 255, 0.1);
// Card glassmorphism
$card-glass-bg: rgba(255, 255, 255, 0.05);
$card-glass-border: rgba(255, 255, 255, 0.1);
$card-glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
// =============================================================================
// 11. TYPOGRAPHY TOKENS (дополнительные)
// =============================================================================
$font-weight-normal: 400;
$font-weight-medium: 500;
$font-weight-semibold: 600;
$font-weight-bold: 700;
$font-size-xs: 11px;
$font-size-sm: 12px;
$font-size-base: 14px;
$font-size-md: 16px;
$font-size-lg: 18px;
// Badge specific
$badge-font-size: 11px;
$badge-font-weight: 600;
// =============================================================================
// 12. LAYOUT TOKENS
// =============================================================================
// Navbar
$navbar-height: 56px;
$navbar-mobile-height: 90px;
$bottom-navbar-height: 60px;
// Sidebar
$sidebar-width: 240px;
$sidebar-collapsed-width: 60px;
// Chat specific
$chat-composer-height: auto;
$chat-message-max-width: 70%;

View File

@ -0,0 +1,279 @@
# Документация: Компонент Buttons
## Обзор
Расширенная система кнопок, дополняющая стандартные кнопки Bootstrap дополнительными вариантами и утилитами.
## Классы кнопок
### 1. Круглые кнопки (`.btn-circle`)
Идеально круглые кнопки для иконок. Автоматически становятся квадратными с `border-radius: 50%`.
#### Базовое использование
```html
<button class="btn btn-primary btn-circle">
<i class="bi-plus"></i>
</button>
<button class="btn btn-outline-secondary btn-circle">
<i class="bi-funnel"></i>
</button>
```
#### Размеры
```html
<!-- Маленькая: 32x32px -->
<button class="btn btn-primary btn-circle btn-sm">
<i class="bi-heart"></i>
</button>
<!-- Средняя (по умолчанию): 36x36px -->
<button class="btn btn-primary btn-circle">
<i class="bi-star"></i>
</button>
<!-- Большая: 44x44px -->
<button class="btn btn-primary btn-circle btn-lg">
<i class="bi-search"></i>
</button>
```
Или используйте специальные классы:
```html
<button class="btn btn-primary btn-circle-sm">...</button>
<button class="btn btn-primary btn-circle-md">...</button>
<button class="btn btn-primary btn-circle-lg">...</button>
```
#### Состояния
Toggle-кнопки автоматически вращаются при раскрытии:
```html
<button class="btn btn-outline-secondary btn-circle"
data-bs-toggle="collapse"
data-bs-target="#filters"
aria-expanded="false">
<i class="bi-funnel"></i>
</button>
<!-- При aria-expanded="true" кнопка повернется на 180° -->
```
### 2. Кнопки с иконками (`.btn-icon`)
Кнопки с иконкой и текстом. На мобильных устройствах текст скрывается.
```html
<button class="btn btn-primary btn-icon">
<i class="bi-plus-lg"></i>
<span class="btn-text">Создать</span>
</button>
<a href="/create" class="btn btn-success btn-icon">
<i class="bi-file-plus"></i>
<span class="btn-text">Новый документ</span>
</a>
```
**Поведение:**
- Десктоп: иконка + текст
- Мобильные (< 768px): только иконка (круглая кнопка)
### 3. Кнопки-призраки (`.btn-ghost`)
Прозрачные кнопки без фона, появляется фон при наведении.
```html
<button class="btn btn-ghost">
<i class="bi-pencil"></i>
Редактировать
</button>
<button class="btn btn-ghost btn-circle">
<i class="bi-three-dots"></i>
</button>
```
### 4. Pill-кнопки (`.btn-pill`)
Максимально скругленные кнопки (`border-radius: 999px`).
```html
<button class="btn btn-primary btn-pill">
Подписаться
</button>
<a href="/more" class="btn btn-outline-secondary btn-pill">
Узнать больше
</a>
```
## Группы кнопок
### Группа круглых кнопок
```html
<div class="btn-circle-group">
<button class="btn btn-outline-secondary btn-circle">
<i class="bi-funnel"></i>
</button>
<button class="btn btn-outline-secondary btn-circle">
<i class="bi-sort-down"></i>
</button>
<button class="btn btn-primary btn-circle">
<i class="bi-plus"></i>
</button>
</div>
```
## Примеры использования
### Пример 1: Шапка страницы с фильтрами
```html
<div class="d-flex align-items-center gap-2">
<h1>Сотрудники</h1>
<div class="ms-auto d-flex gap-2">
<!-- Поиск -->
<input type="search" class="form-control" placeholder="Поиск...">
<!-- Кнопка фильтров -->
<button class="btn btn-outline-secondary btn-circle"
data-bs-toggle="collapse"
data-bs-target="#filters">
<i class="bi-funnel"></i>
</button>
<!-- Кнопка создания -->
<button class="btn btn-primary btn-icon">
<i class="bi-plus-lg"></i>
<span class="btn-text">Создать</span>
</button>
</div>
</div>
```
### Пример 2: Карточка с действиями
```html
<div class="card">
<div class="card-body">
<h5>Название документа</h5>
<p>Описание...</p>
<div class="btn-circle-group">
<button class="btn btn-ghost btn-circle" title="Редактировать">
<i class="bi-pencil"></i>
</button>
<button class="btn btn-ghost btn-circle" title="Поделиться">
<i class="bi-share"></i>
</button>
<button class="btn btn-ghost btn-circle" title="Удалить">
<i class="bi-trash"></i>
</button>
</div>
</div>
</div>
```
### Пример 3: Навигация с разными размерами
```html
<div class="d-flex align-items-center gap-3">
<!-- Маленькие кнопки для вспомогательных действий -->
<button class="btn btn-outline-secondary btn-circle btn-sm">
<i class="bi-arrow-left"></i>
</button>
<!-- Обычная кнопка для основного действия -->
<button class="btn btn-primary btn-pill">
Далее
</button>
<!-- Маленькая кнопка справа -->
<button class="btn btn-outline-secondary btn-circle btn-sm">
<i class="bi-arrow-right"></i>
</button>
</div>
```
## Комбинирование с Bootstrap
Все новые классы полностью совместимы со стандартными классами Bootstrap:
```html
<!-- Цвета Bootstrap -->
<button class="btn btn-primary btn-circle">...</button>
<button class="btn btn-success btn-circle">...</button>
<button class="btn btn-danger btn-circle">...</button>
<button class="btn btn-outline-secondary btn-circle">...</button>
<!-- Размеры Bootstrap + круглые кнопки -->
<button class="btn btn-primary btn-circle btn-sm">...</button>
<button class="btn btn-primary btn-circle btn-lg">...</button>
<!-- Состояния Bootstrap -->
<button class="btn btn-primary btn-circle" disabled>...</button>
<button class="btn btn-outline-secondary btn-circle active">...</button>
```
## Миграция со старого кода
### Было (специфичные стили):
```html
<button class="btn btn-outline-secondary page-filter-toggle">
<i class="bi-funnel"></i>
</button>
```
```css
.page-filter-toggle {
width: 36px !important;
height: 36px !important;
border-radius: 50% !important;
/* ... еще 20 строк CSS ... */
}
```
### Стало (универсальный класс):
```html
<button class="btn btn-outline-secondary btn-circle">
<i class="bi-funnel"></i>
</button>
```
```css
/* Никаких дополнительных стилей не нужно! */
```
## CSS переменные
Компонент использует стандартные переменные Bootstrap:
- `--bs-primary` - основной цвет
- `--bs-body-color` - цвет текста
- `--bs-border-color` - цвет границ
- `--bs-border-radius` - скругление углов
## Поддержка темной темы
Все кнопки автоматически адаптируются под темную тему через `prefers-color-scheme: dark`.
## Доступность
- Все кнопки имеют правильные `aria-label` атрибуты
- Иконки используют `line-height: 1` для правильного центрирования
- Фокус-стейты наследуются от Bootstrap
- Поддержка клавиатурной навигации
## Производительность
- Минимальное использование `!important` (только где абсолютно необходимо)
- CSS переменные для легкой кастомизации
- Оптимизированные селекторы
- Не влияет на другие компоненты

View File

@ -0,0 +1,232 @@
// =============================================================================
// Badges Component
// =============================================================================
// Notification badges with Telegram-style design
// Uses design tokens from abstracts/_variables.scss
// =============================================================================
// 1. BASE COMPONENT
// =============================================================================
.badge {
@include badge-base($badge-height-base, $badge-radius);
@include badge-shadow();
color: $color-white;
background: var(--bs-danger);
transition: $badge-transition;
user-select: none;
padding: 0 6px;
// Auto-hide when empty
&:empty {
display: none;
}
// Hide when count is 0
&[data-count="0"] {
display: none;
}
}
// =============================================================================
// 2. SIZE MODIFIERS
// =============================================================================
.badge--sm {
@include badge-base($badge-height-sm, $badge-radius-sm);
font-size: 10px;
padding: 0 5px;
}
.badge--lg {
@include badge-base($badge-height-lg, $badge-radius-lg);
font-size: 12px;
padding: 0 7px;
}
.badge--xl {
@include badge-base($badge-height-xl, $badge-radius-xl);
font-size: 13px;
padding: 0 8px;
}
// =============================================================================
// 3. COLOR MODIFIERS
// =============================================================================
.badge--primary {
background: var(--bs-primary);
}
.badge--secondary {
background: var(--bs-secondary);
opacity: 0.8;
}
.badge--success {
background: var(--bs-success);
}
.badge--warning {
background: var(--bs-warning);
color: var(--bs-dark);
}
.badge--info {
background: var(--bs-info);
}
.badge--mention {
background: $telegram-blue;
}
.badge--muted {
background: var(--bs-secondary);
opacity: 0.6;
color: var(--bs-secondary-color);
}
// =============================================================================
// 4. STYLE MODIFIERS
// =============================================================================
.badge--outlined {
background: transparent;
border: 2px solid currentColor;
color: var(--bs-danger);
}
.badge--dot {
width: 8px;
height: 8px;
min-width: 8px;
padding: 0;
@include radius(50%);
}
.badge--inline {
display: inline-flex;
vertical-align: middle;
margin-left: 4px;
}
// Premium badge with gradient
.badge--premium {
background: linear-gradient(135deg, $gradient-purple-start 0%, $gradient-purple-end 100%);
}
.badge--star {
background: linear-gradient(135deg, $gradient-pink-start 0%, $gradient-pink-end 100%);
}
// =============================================================================
// 5. CONTEXT MODIFIERS
// =============================================================================
// Badge on avatar
.badge--avatar {
position: absolute;
top: 0;
right: 0;
transform: translate(25%, -25%);
z-index: $z-base;
}
// Badge on button
.badge--button {
position: absolute;
top: -6px;
right: -6px;
}
// Interactive badge (clickable)
.badge--interactive {
cursor: pointer;
@include transition-all($transition-fast);
&:hover {
transform: scale(1.1);
box-shadow: $badge-shadow-hover;
}
&:active {
transform: scale(0.95);
}
}
// =============================================================================
// 6. STATES
// =============================================================================
.badge.is-hidden {
display: none;
}
.badge.is-animating-in {
animation: badge-scale-in 0.2s $ease-bounce;
}
.badge.is-pulsing {
animation: badge-pulse 1.5s infinite;
}
.badge.is-animating-out {
animation: badge-scale-out 0.2s $ease-bounce forwards;
}
// =============================================================================
// 7. DARK MODE
// =============================================================================
[data-bs-theme="dark"] {
.badge {
box-shadow: $badge-shadow-dark;
}
.badge--outlined {
border-width: 1px;
}
.badge--avatar {
height: calc($badge-height-base - 2px);
min-width: calc($badge-height-base - 2px);
}
}
// =============================================================================
// 8. ANIMATIONS
// =============================================================================
@keyframes badge-scale-in {
from {
transform: scale(0);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
@keyframes badge-pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.15);
}
100% {
transform: scale(1);
}
}
@keyframes badge-scale-out {
from {
transform: scale(1);
opacity: 1;
}
to {
transform: scale(0);
opacity: 0;
}
}

View File

@ -0,0 +1,233 @@
// =============================================================================
// Telegram-Style Badges
// =============================================================================
// Based on Telegram Web K design system (morethanwords/tweb)
// Perfect circles, smooth animations, professional appearance
// REFACTORED: использует design tokens из abstracts/_variables.scss
// =============================================================================
// Base Badge Class
// =============================================================================
.badge-telegram {
@include badge-base($badge-height-base, $badge-radius);
color: $color-white;
background: var(--bs-danger);
@include badge-shadow();
transition: $badge-transition;
user-select: none;
padding: 0 $badge-padding-x;
// Auto-hide when empty
&:empty {
display: none;
}
// Hide when count is 0
&[data-count="0"] {
display: none;
}
}
// =============================================================================
// Size Variants
// =============================================================================
.badge-telegram-sm {
@include badge-base($badge-height-sm, $badge-radius-sm);
font-size: 10px;
padding: 0 5px;
}
.badge-telegram-lg {
@include badge-base($badge-height-lg, $badge-radius-lg);
font-size: 12px;
padding: 0 7px;
}
.badge-telegram-xl {
@include badge-base($badge-height-xl, $badge-radius-xl);
font-size: 13px;
padding: 0 8px;
}
// =============================================================================
// Color Variants
// =============================================================================
.badge-telegram-primary {
background: var(--bs-primary);
}
.badge-telegram-muted {
background: var(--bs-secondary);
opacity: 0.8;
}
.badge-telegram-success {
background: var(--bs-success);
}
.badge-telegram-warning {
background: var(--bs-warning);
color: var(--bs-dark);
}
.badge-telegram-info {
background: var(--bs-info);
}
.badge-telegram-mention {
background: $telegram-blue;
}
// =============================================================================
// Animations
// =============================================================================
@keyframes badge-scale-in {
from {
transform: scale(0);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
@keyframes badge-pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.15);
}
100% {
transform: scale(1);
}
}
@keyframes badge-scale-out {
from {
transform: scale(1);
opacity: 1;
}
to {
transform: scale(0);
opacity: 0;
}
}
// Animation Classes
.badge-telegram.animate-in {
animation: badge-scale-in 0.2s $ease-bounce;
}
.badge-telegram.pulse {
animation: badge-pulse 0.6s $badge-transition;
}
.badge-telegram.animate-out {
animation: badge-scale-out 0.2s $badge-transition;
}
// =============================================================================
// Positioning Variants
// =============================================================================
// On avatar
.badge-telegram-avatar {
position: absolute;
bottom: -2px;
right: -2px;
border: 2px solid white;
}
// On button/icon
.badge-telegram-button {
position: absolute;
top: -6px;
right: -10px;
z-index: 1;
}
// Inline with text
.badge-telegram-inline {
position: relative;
top: -1px;
margin-left: 4px;
}
// =============================================================================
// Special Effects
// =============================================================================
// Dot badge (no number)
.badge-telegram-dot {
width: 8px;
height: 8px;
min-width: 8px;
padding: 0;
border-radius: 50%;
}
// Interactive badge (hoverable)
.badge-telegram-interactive {
cursor: pointer;
transition: all 0.2s ease;
&:hover {
transform: scale(1.1);
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.2);
}
&:active {
transform: scale(0.95);
}
}
// Premium badge (gradient)
.badge-telegram-premium {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
// Star badge (for favorites)
.badge-telegram-star {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
// =============================================================================
// Responsive Design
// =============================================================================
@media (max-width: 576px) {
.badge-telegram {
height: calc($badge-height-base - 2px);
min-width: calc($badge-height-base - 2px);
font-size: calc($badge-font-size - 1px);
padding: 0 calc($badge-padding-x - 1px);
}
}
// =============================================================================
// Dark Theme Support
// =============================================================================
@media (prefers-color-scheme: dark) {
.badge-telegram {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.badge-telegram-avatar {
border-color: var(--bs-dark);
}
}
// =============================================================================
// Utilities
// =============================================================================
// Hide badge class (for JavaScript manipulation)
.badge-telegram-hidden {
display: none !important;
}
// Badge with border
.badge-telegram-outlined {
background: transparent;
color: var(--bs-danger);
border: 2px solid var(--bs-danger);
box-shadow: none;
}

View File

@ -0,0 +1,193 @@
// =============================================================================
// Custom Button Styles (SCSS)
// =============================================================================
// Расширение стандартных кнопок Bootstrap с дополнительными вариантами:
// - .btn-circle - круглая кнопка
// - .btn-icon - кнопка с иконкой и текстом
// - .btn-ghost - кнопка-призрак
// - .btn-pill - максимально скругленная кнопка
// =============================================================================
// ПЕРЕМЕННЫЕ
// =============================================================================
// Размеры круглых кнопок (используем значения, кратные 4px)
$btn-circle-size-sm: 32px;
$btn-circle-size-md: 36px;
$btn-circle-size-lg: 44px;
// Размеры шрифтов
$btn-circle-font-sm: 0.875rem;
$btn-circle-font-md: 1rem;
$btn-circle-font-lg: 1.25rem;
// Отступы
$btn-icon-gap: var(--space-2); // 8px
$btn-circle-mobile-size: 36px;
$btn-circle-mobile-padding: var(--space-2); // 8px
// Переходы
$btn-transition: all 0.2s ease;
$btn-toggle-transition: transform 0.3s ease;
// =============================================================================
// КРУГЛЫЕ КНОПКИ
// =============================================================================
// Миксин для размеров круглых кнопок (DRY)
@mixin btn-circle-size($size, $font-size) {
width: $size !important;
height: $size !important;
min-width: $size !important;
max-width: $size !important;
min-height: $size !important;
max-height: $size !important;
font-size: $font-size;
}
// Базовый класс для круглых кнопок
.btn-circle {
border-radius: 50% !important;
padding: 0 !important;
display: inline-flex !important;
align-items: center;
justify-content: center;
flex-shrink: 0;
aspect-ratio: 1 / 1;
overflow: hidden;
line-height: 1;
// Иконка внутри круглой кнопки
i {
line-height: 1;
display: inline-block;
}
}
// Размеры круглых кнопок
.btn-circle.btn-sm,
.btn-circle-sm {
@include btn-circle-size($btn-circle-size-sm, $btn-circle-font-sm);
}
.btn-circle:not(.btn-sm):not(.btn-lg),
.btn-circle-md {
@include btn-circle-size($btn-circle-size-md, $btn-circle-font-md);
}
.btn-circle.btn-lg,
.btn-circle-lg {
@include btn-circle-size($btn-circle-size-lg, $btn-circle-font-lg);
}
// =============================================================================
// КНОПКИ С ИКОНКАМИ
// =============================================================================
// Кнопка с иконкой и текстом
.btn-icon {
display: inline-flex;
align-items: center;
gap: $btn-icon-gap;
i {
line-height: 1;
}
}
// Адаптив: только иконка на мобильных
@media (max-width: 767.98px) {
.btn-icon {
.btn-text {
display: none;
}
// Если это не круглая кнопка, делаем её круглой на мобильных
&:not(.btn-circle) {
border-radius: 50% !important;
padding: $btn-circle-mobile-padding !important;
width: $btn-circle-mobile-size;
height: $btn-circle-mobile-size;
justify-content: center;
}
}
}
// =============================================================================
// ДОПОЛНИТЕЛЬНЫЕ ВАРИАНТЫ
// =============================================================================
// Кнопка-призрак (без фона, только border при hover)
.btn-ghost {
background: transparent;
border: 1px solid transparent;
color: var(--bs-body-color);
transition: $btn-transition;
&:hover {
background: color-mix(in srgb, var(--bs-body-color), transparent 90%);
border-color: var(--bs-border-color);
color: var(--bs-body-color);
}
&:active {
background: color-mix(in srgb, var(--bs-body-color), transparent 85%);
}
}
// Pill-кнопки (максимально скругленные)
.btn-pill {
border-radius: 999px !important;
}
// =============================================================================
// СОСТОЯНИЯ
// =============================================================================
// Активное состояние для toggle-кнопок
.btn-circle[aria-expanded="true"],
.btn-circle.active {
background-color: var(--bs-primary) !important;
border-color: var(--bs-primary) !important;
color: white !important;
}
// Анимация вращения для toggle-кнопок
.btn-circle[aria-expanded="true"] {
transform: rotate(180deg);
transition: $btn-toggle-transition;
}
.btn-circle[aria-expanded="false"] {
transform: rotate(0deg);
transition: $btn-toggle-transition;
}
// =============================================================================
// ГРУППЫ КРУГЛЫХ КНОПОК
// =============================================================================
// Группа круглых кнопок с небольшим отступом
.btn-circle-group {
display: flex;
gap: var(--space-2);
align-items: center;
.btn-circle {
margin: 0;
}
}
// =============================================================================
// ТЕМНАЯ ТЕМА
// =============================================================================
@media (prefers-color-scheme: dark) {
.btn-ghost {
color: var(--bs-body-color);
&:hover {
background: color-mix(in srgb, var(--bs-body-color), transparent 92%);
}
}
}

View File

@ -0,0 +1,317 @@
// =============================================================================
// Card List Component (SCSS)
// =============================================================================
// Универсальные стили для карточек в списках: новости, заявления, документы
// Классы feed-* применяются для любых типов контента
//
// Использование:
// - .card — контейнер карточки
// - .card-header — шапка карточки (аватар + мета)
// - .card-body — тело карточки
// - .card-actions — кнопки действий
// - .card-list — контейнер потока карточек
// =============================================================================
// ПЕРЕМЕННЫЕ
// =============================================================================
// Карточки
$card-icon-size: 42px;
$card-icon-size-sm: 36px;
$card-icon-size-compact: 32px;
$card-icon-border-radius: 9999px;
// Переходы
$card-transition: box-shadow 0.18s ease, transform 0.18s ease, border-color 0.18s ease;
$card-action-transition: background 0.2s ease, color 0.2s ease;
// Тени
$card-hover-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
// Границы
$card-highlighted-border: 3px solid var(--bs-primary);
// Отступы
$feed-title-margin: var(--space-1) 0;
// =============================================================================
// ЗАГОЛОВОК СЕКЦИИ
// =============================================================================
.section-header {
display: flex;
align-items: center;
gap: var(--gap-component);
margin-bottom: var(--space-3);
.title {
font-weight: 800;
letter-spacing: 0.2px;
margin: 0;
}
}
// =============================================================================
// КОНТЕЙНЕР ПОТОКА КАРТОЧЕК
// =============================================================================
.card-list {
display: flex;
flex-direction: column;
gap: 0;
}
// =============================================================================
// БАЗОВАЯ КАРТОЧКА
// =============================================================================
.card {
border: 1px solid color-mix(in srgb, var(--bs-border-color), transparent 10%);
border-radius: var(--feed-radius, 18px);
background: var(--bs-body-bg);
box-shadow: var(--feed-shadow, 0 2px 8px rgba(0, 0, 0, 0.08));
transition: $card-transition;
overflow: hidden;
&:hover {
box-shadow: $card-hover-shadow;
transform: translateY(-1px);
}
// Вариации карточек
&.compact {
.card-header {
padding: var(--space-1) var(--card-padding-sm);
}
.card-body {
padding: var(--space-1) var(--card-padding-sm);
}
.card-icon {
width: $card-icon-size-compact;
height: $card-icon-size-compact;
}
}
&.borderless {
border: none;
box-shadow: none;
border-radius: 0;
border-bottom: 1px solid var(--bs-border-color);
&:hover {
transform: none;
box-shadow: none;
background: var(--bs-tertiary-bg);
}
}
&.highlighted {
border-left: $card-highlighted-border;
background: color-mix(in srgb, var(--bs-primary), transparent 95%);
}
}
// =============================================================================
// ШАПКА КАРТОЧКИ
// =============================================================================
.card-header {
display: flex;
align-items: center;
gap: var(--gap-component);
padding: var(--card-padding);
padding-bottom: var(--space-1);
}
// Аватар
.card-icon {
width: $card-icon-size;
height: $card-icon-size;
border-radius: $card-icon-border-radius;
overflow: hidden;
border: 1px solid color-mix(in srgb, var(--bs-border-color), transparent 20%);
background: var(--bs-tertiary-bg);
display: grid;
place-items: center;
flex: 0 0 auto;
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
}
// Мета-информация
.card-meta {
line-height: 1.2;
flex: 1;
min-width: 0; /* для корректного text-overflow */
}
.card-title {
font-weight: 700;
color: var(--bs-body-color);
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-subtitle {
font-size: 0.82rem;
color: var(--bs-secondary-color);
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
// Значок закрепления
.feed-pin {
margin-left: auto;
font-size: 0.75rem;
padding: var(--space-1);
border-radius: 999px;
background: #fff3cd;
color: #664d03;
border: 1px solid #ffe69c;
flex-shrink: 0;
[data-bs-theme="dark"] & {
background: #3b2e04;
color: #ffda6a;
border-color: #4f3f0a;
}
}
// =============================================================================
// ТЕЛО КАРТОЧКИ
// =============================================================================
.card-body {
padding: var(--space-1) var(--card-padding);
padding-bottom: var(--space-1);
position: relative;
}
.feed-title {
font: 700 1.3rem/1.5 var(--bs-body-font-family);
margin: $feed-title-margin;
color: #4b4b4b;
[data-bs-theme="dark"] & {
color: var(--bs-body-color);
}
}
.feed-text {
font-size: 0.95rem;
line-height: 1.6;
color: var(--bs-body-color);
margin: 0;
}
// =============================================================================
// ИЗОБРАЖЕНИЕ В КАРТОЧКЕ
// =============================================================================
.feed-img {
position: relative;
border-top: 1px solid var(--bs-border-color);
background: var(--bs-tertiary-bg);
img {
display: block;
width: 100%;
height: auto;
aspect-ratio: 16/9;
object-fit: cover;
}
}
// =============================================================================
// ДЕЙСТВИЯ (ЛАЙКИ, КОММЕНТАРИИ)
// =============================================================================
.card-actions {
display: flex;
align-items: center;
gap: var(--space-1);
padding: var(--card-padding);
padding-top: var(--space-2);
border-top: 1px solid var(--bs-border-color);
position: relative;
z-index: 2;
}
.feed-action {
display: inline-flex;
align-items: center;
gap: var(--space-1);
padding: var(--space-1);
border-radius: var(--radius-md, 10px);
background: transparent;
border: none;
color: var(--bs-secondary-color);
font-size: 0.875rem;
cursor: pointer;
transition: $card-action-transition;
&:hover {
background: var(--bs-tertiary-bg);
color: var(--bs-body-color);
}
&.active {
color: var(--bs-primary);
font-weight: 600;
}
i {
font-size: 1rem;
}
}
// =============================================================================
// ФУТЕР КАРТОЧКИ
// =============================================================================
.feed-footer {
padding: var(--space-2) var(--card-padding);
border-top: 1px solid var(--bs-border-color);
font-size: 0.875rem;
color: var(--bs-secondary-color);
}
// =============================================================================
// АДАПТИВНОСТЬ
// =============================================================================
@media (max-width: 576px) {
.section-header {
gap: var(--space-2);
flex-wrap: wrap;
}
.card-header {
padding: var(--space-2);
padding-bottom: var(--space-1);
}
.card-icon {
width: $card-icon-size-sm;
height: $card-icon-size-sm;
}
.feed-title {
font-size: 1.1rem;
}
.card-actions {
padding: var(--space-2);
}
}

View File

@ -0,0 +1,341 @@
// =============================================================================
// Cards Component
// =============================================================================
// Glassmorphism карточки с поддержкой тёмной темы
// =============================================================================
// 1. БАЗОВЫЕ СТИЛИ КАРТОЧКИ
// =============================================================================
.card {
// Применяем glassmorphism эффект
@include glassmorphism-card();
// Прозрачный фон
--bs-card-bg: transparent;
background: $glass-light-bg !important;
// Тонкая рамка
border: 1px solid $glass-light-border !important;
// Плавные переходы
@include transition-all($transition-medium);
overflow: hidden;
// Hover эффект - усиливаем glassmorphism
&:hover {
background: rgba(255, 255, 255, 0.15) !important;
border-color: rgba(0, 0, 0, 0.12) !important;
backdrop-filter: $blur-lg saturate(200%);
-webkit-backdrop-filter: $blur-lg saturate(200%);
box-shadow: 0 12px 48px 0 rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
}
// =============================================================================
// 2. ТЁМНАЯ ТЕМА
// =============================================================================
[data-bs-theme="dark"] {
.card {
--bs-card-bg: transparent;
--bs-card-border-color: $glass-dark-border;
background: transparent !important;
border-color: $glass-dark-border !important;
&:hover {
background: rgba(255, 255, 255, 0.03) !important;
border-color: rgba(255, 255, 255, 0.15) !important;
}
}
}
// =============================================================================
// 3. LAYOUT КОМПОНЕНТЫ КАРТОЧЕК
// =============================================================================
// Контейнер списка карточек
.card-list {
display: flex;
flex-direction: column;
gap: 0;
}
// Заголовок секции
.section-header {
@include flex-align-center();
gap: var(--gap-component, 1rem);
margin-bottom: var(--space-3, 0.75rem);
.title {
font-weight: 800;
letter-spacing: 0.2px;
margin: 0;
}
}
// =============================================================================
// 4. СТРУКТУРА КАРТОЧКИ
// =============================================================================
// Card header
.card-header {
@include flex-align-center();
gap: var(--gap-component, 1rem);
padding: var(--card-padding, 1rem);
padding-bottom: var(--space-1, 0.25rem);
background: transparent;
border-bottom: none;
}
// Иконка/аватар в карточке
.card-icon {
width: 42px;
height: 42px;
@include circle();
overflow: hidden;
border: 1px solid color-mix(in srgb, var(--bs-border-color), transparent 20%);
background: var(--bs-tertiary-bg);
@include flex-center();
flex: 0 0 auto;
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
}
// Метаинформация
.card-meta {
line-height: 1.2;
flex: 1;
min-width: 0;
}
// Заголовок карточки
.card-title {
font-weight: 700;
color: var(--bs-body-color);
@include text-truncate();
display: block;
margin-bottom: 0;
}
// Подзаголовок
.card-subtitle {
font-size: 0.82rem;
color: var(--bs-secondary-color);
@include text-truncate();
display: block;
margin-bottom: 0;
}
// Тело карточки
.card-body {
padding: var(--space-1, 0.25rem) var(--card-padding, 1rem);
padding-bottom: var(--space-1, 0.25rem);
position: relative;
}
// =============================================================================
// 5. ДЕЙСТВИЯ КАРТОЧКИ (CARD ACTIONS)
// =============================================================================
.card-actions {
@include flex-align-center();
gap: var(--space-1, 0.25rem);
padding: var(--card-padding, 1rem);
padding-top: var(--space-2, 0.5rem);
border-top: 1px solid var(--bs-border-color);
position: relative;
z-index: $z-base;
// Активные кнопки в actions
.btn,
button {
&.active,
&.is-liked {
color: var(--bs-primary) !important;
font-weight: 600;
}
}
}
// =============================================================================
// 6. ВАРИАЦИИ КАРТОЧЕК
// =============================================================================
// Компактная карточка
.card.compact {
.card-header {
padding: var(--space-1, 0.25rem) var(--card-padding-sm, 0.5rem);
}
.card-body {
padding: var(--space-1, 0.25rem) var(--card-padding-sm, 0.5rem);
}
.card-icon {
width: 32px;
height: 32px;
}
}
// Карточка без рамки
.card.borderless {
border: none;
@include no-shadow();
border-radius: 0;
border-bottom: 1px solid var(--bs-border-color);
&:hover {
transform: none;
@include no-shadow();
background: var(--bs-tertiary-bg);
}
}
// Выделенная карточка
.card.highlighted {
border-left: 3px solid var(--bs-primary);
background: color-mix(in srgb, var(--bs-primary), transparent 95%);
}
// =============================================================================
// 7. RESPONSIVE
// =============================================================================
@include mobile() {
.section-header {
gap: var(--space-2, 0.5rem);
flex-wrap: wrap;
}
.card-header {
padding: var(--space-2, 0.5rem);
padding-bottom: var(--space-1, 0.25rem);
}
.card-icon {
width: 36px;
height: 36px;
}
.card-actions {
padding: var(--space-2, 0.5rem);
}
}
// =============================================================================
// 8. СТИЛИ КНОПОК (из custom-bootstrap.scss)
// =============================================================================
// Базовые стили для ВСЕХ кнопок - полностью прозрачные
.btn {
background: transparent !important;
border: none !important;
color: var(--bs-secondary-color);
font-weight: 500;
@include transition(color, $transition-fast);
&:hover:not(:disabled) {
color: var(--bs-body-color);
}
&:active:not(:disabled) {
color: var(--bs-body-color);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
// Primary кнопка (единственная с цветным фоном)
.btn-primary {
color: $color-white !important;
background: rgba(13, 110, 253, 0.7) !important;
&:hover:not(:disabled) {
background: rgba(13, 110, 253, 0.85) !important;
box-shadow: 0 4px 16px rgba(13, 110, 253, 0.3);
}
&:active:not(:disabled) {
background: rgba(13, 110, 253, 0.95) !important;
}
}
// Secondary, outline, ghost - все прозрачные
.btn-secondary,
.btn-outline-primary,
.btn-outline-secondary,
.btn-ghost {
background: transparent !important;
color: var(--bs-secondary-color);
&:hover:not(:disabled) {
background: transparent !important;
color: var(--bs-body-color);
}
&:active:not(:disabled) {
background: transparent !important;
}
}
// Особый цвет для outline-primary
.btn-outline-primary {
color: var(--bs-primary);
&:hover:not(:disabled) {
color: var(--bs-primary);
background: transparent !important;
}
}
// Кнопки в тёмной теме
[data-bs-theme="dark"] {
.btn {
&:hover:not(:disabled),
&:active:not(:disabled) {
background: transparent !important;
}
}
.btn-primary {
background: rgba(13, 110, 253, 0.6) !important;
&:hover:not(:disabled) {
background: rgba(13, 110, 253, 0.75) !important;
}
&:active:not(:disabled) {
background: rgba(13, 110, 253, 0.85) !important;
}
}
.btn-secondary,
.btn-outline-primary,
.btn-outline-secondary,
.btn-ghost {
&:hover:not(:disabled),
&:active:not(:disabled) {
background: transparent !important;
}
}
}
// =============================================================================
// 9. УТИЛИТАРНЫЕ КЛАССЫ
// =============================================================================
// Моноширинный шрифт для дат, цифр, кодов
.text-mono {
font-variant-numeric: tabular-nums;
font-family: ui-monospace, 'SF Mono', SFMono-Regular, 'Consolas', 'Liberation Mono', 'Menlo', 'Courier', monospace;
}

View File

@ -0,0 +1,152 @@
// =============================================================================
// Comments Component
// =============================================================================
// Единый стиль для комментариев во всех местах приложения:
// - В карточках постов (_feed_cards.html)
// - На странице деталей поста (post_detail.html)
// - В модальном окне (postDetailModal.js)
.comment-item {
padding: 0.5rem 0; // py-2
display: flex;
flex-direction: column;
align-items: flex-start;
.comment-header {
font-size: 0.875rem; // small
margin-bottom: 0.25rem;
display: flex;
align-items: center;
flex-wrap: wrap;
.comment-author {
font-weight: 600; // fw-semibold
text-decoration: none;
color: inherit;
&:hover {
text-decoration: underline;
}
}
.comment-separator {
color: var(--bs-secondary);
margin: 0 0.25rem;
}
.comment-date {
color: var(--bs-secondary);
}
.badge {
font-size: 0.7rem;
padding: 0.15rem 0.4rem;
}
}
.comment-body {
font-size: 0.875rem; // small
line-height: 1.5;
white-space: pre-wrap;
word-wrap: break-word;
background: var(--bs-tertiary-bg);
padding: 0.5rem 0.75rem;
border-radius: 12px;
max-width: 80%;
}
// Комментарии автора поста справа
&.comment-item--author {
align-items: flex-end;
.comment-header {
flex-direction: row-reverse;
.comment-separator {
order: 0;
}
.comment-date {
order: -1;
}
}
.comment-body {
background: var(--bs-primary-bg-subtle, rgba(13, 110, 253, 0.1));
border: 1px solid var(--bs-primary-border-subtle, rgba(13, 110, 253, 0.2));
}
}
}
// Контейнер для списка комментариев
.comments-list {
margin-bottom: 1rem;
}
// Заголовок секции комментариев
.comments-section-header {
display: flex;
align-items: center;
margin-bottom: 0.5rem;
.comments-icon {
font-size: 1.5rem;
color: var(--bs-primary);
margin-right: 0.5rem;
}
.comments-title {
margin-bottom: 0;
}
.comments-count {
font-size: 0.875rem;
color: var(--bs-secondary);
margin-left: 0.5rem;
}
}
// Обёртка для комментариев с sticky формой
.comments-section-wrapper {
position: relative;
}
// Скроллируемая область с комментариями
.comments-scrollable {
max-height: 400px;
overflow-y: auto;
overflow-x: hidden;
padding-bottom: 1rem;
// Firefox скроллбар
scrollbar-width: thin;
scrollbar-color: rgba(0, 0, 0, 0.15) transparent;
// Стилизация скроллбара (Chrome, Safari, Edge)
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.15);
border-radius: 4px;
&:hover {
background: rgba(0, 0, 0, 0.25);
}
}
}
// Sticky форма для комментариев
.comment-form-sticky {
position: sticky;
bottom: 0;
background: var(--bs-body-bg);
z-index: 10;
border-top: 1px solid var(--bs-border-color);
padding-top: 1rem;
}

View File

@ -0,0 +1,327 @@
/**
* _common.scss
* Общие переиспользуемые классы для всех компонентов
* Избегает дублирования между компонентами
*/
/* ─────────────────────────────────────────────────────────────
BUTTONS
────────────────────────────────────────────────────────────── */
/* Иконочная кнопка */
.btn-icon {
width: 36px;
height: 36px;
display: inline-grid;
place-items: center;
border: 1px solid var(--bs-border-color);
border-radius: var(--button-radius, 10px);
background: transparent;
color: var(--bs-body-color);
cursor: pointer;
transition: all var(--transition-fast, 0.15s);
&:hover {
background: var(--bs-tertiary-bg);
border-color: var(--bs-border-color);
}
&--primary {
color: var(--bs-primary);
}
&--danger {
color: var(--bs-danger);
}
&--success {
color: var(--bs-success);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
/* ─────────────────────────────────────────────────────────────
BADGES
────────────────────────────────────────────────────────────── */
/* Бейдж подтверждения */
.badge-acked {
background: color-mix(in srgb, var(--bs-success) 20%, transparent);
color: var(--bs-success);
border: 1px solid color-mix(in srgb, var(--bs-success) 30%, transparent);
padding: 0.28rem 0.55rem;
border-radius: var(--badge-radius, 999px);
font-size: 0.8rem;
font-weight: 500;
[data-bs-theme="dark"] & {
background: color-mix(in srgb, var(--bs-success) 15%, #000);
color: color-mix(in srgb, var(--bs-success) 80%, #fff);
border-color: color-mix(in srgb, var(--bs-success) 25%, transparent);
}
}
/* Бейдж статуса */
.badge-status {
padding: var(--space-1);
border-radius: var(--badge-radius, 999px);
font-size: 0.75rem;
font-weight: 600;
border: 1px solid;
&--pending {
background: color-mix(in srgb, var(--bs-warning) 15%, transparent);
color: var(--bs-warning);
border-color: color-mix(in srgb, var(--bs-warning) 30%, transparent);
}
&--approved {
background: color-mix(in srgb, var(--bs-success) 15%, transparent);
color: var(--bs-success);
border-color: color-mix(in srgb, var(--bs-success) 30%, transparent);
}
&--rejected {
background: color-mix(in srgb, var(--bs-danger) 15%, transparent);
color: var(--bs-danger);
border-color: color-mix(in srgb, var(--bs-danger) 30%, transparent);
}
}
/* ─────────────────────────────────────────────────────────────
PINNED BADGE (централизованный)
────────────────────────────────────────────────────────────── */
.chat-pinned-badge {
position: absolute;
background: var(--bs-warning);
color: white;
@include circle();
@include flex-center();
z-index: 1;
// Размер по умолчанию (средний)
width: 22px;
height: 22px;
top: 6px;
right: 6px;
font-size: 0.75rem;
// Маленький вариант (для списка чатов)
&--sm {
width: 20px;
height: 20px;
top: 4px;
right: 4px;
font-size: 0.7rem;
}
// Большой вариант (для детального чата)
&--lg {
width: 24px;
height: 24px;
top: 8px;
right: 8px;
font-size: 0.8rem;
}
}
/* ─────────────────────────────────────────────────────────────
AVATARS
────────────────────────────────────────────────────────────── */
.avatar {
border-radius: 50%;
overflow: hidden;
display: inline-flex;
align-items: center;
justify-content: center;
background: var(--bs-tertiary-bg);
border: 1px solid color-mix(in srgb, var(--bs-border-color), transparent 20%);
flex-shrink: 0;
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
i {
color: var(--bs-secondary-color);
}
}
/* Размеры */
.avatar-xs {
width: var(--avatar-xs, 28px);
height: var(--avatar-xs, 28px);
}
.avatar-sm {
width: var(--avatar-sm, 36px);
height: var(--avatar-sm, 36px);
}
.avatar-md {
width: var(--avatar-md, 44px);
height: var(--avatar-md, 44px);
}
.avatar-lg {
width: var(--avatar-lg, 64px);
height: var(--avatar-lg, 64px);
}
.avatar-xl {
width: var(--avatar-xl, 96px);
height: var(--avatar-xl, 96px);
}
/* ─────────────────────────────────────────────────────────────
FEED AUTHOR & META
────────────────────────────────────────────────────────────── */
.card-title {
font-weight: 700;
color: var(--bs-body-color);
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-subtitle {
font-size: 0.82rem;
color: var(--bs-secondary-color);
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
a {
color: inherit;
text-decoration: none;
&:hover {
color: var(--bs-primary);
}
}
}
/* ─────────────────────────────────────────────────────────────
AVATARS (Centralized)
────────────────────────────────────────────────────────────── */
.chat-avatar,
.avatar {
@include circle();
overflow: hidden;
flex-shrink: 0;
background: var(--bs-tertiary-bg);
border: 1px solid var(--bs-border-color);
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
}
// Size variants
.chat-avatar,
.avatar {
width: $avatar-md;
height: $avatar-md;
}
.chat-avatar--sm,
.avatar--sm {
width: $avatar-sm;
height: $avatar-sm;
}
.chat-avatar--lg,
.avatar--lg {
width: $avatar-lg;
height: $avatar-lg;
}
.chat-avatar--xl,
.avatar--xl {
width: $avatar-xl;
height: $avatar-xl;
}
/* ─────────────────────────────────────────────────────────────
CHIPS
────────────────────────────────────────────────────────────── */
.chip {
display: inline-flex;
align-items: center;
gap: var(--space-1);
padding: var(--space-1);
border-radius: var(--badge-radius, 999px);
background: var(--bs-tertiary-bg);
border: 1px solid var(--bs-border-color);
font-size: 0.82rem;
color: var(--bs-body-color);
}
.chip-remove {
cursor: pointer;
color: var(--bs-danger);
font-weight: 700;
line-height: 1;
&:hover {
color: color-mix(in srgb, var(--bs-danger) 80%, #000);
}
}
/* ─────────────────────────────────────────────────────────────
UTILITIES
────────────────────────────────────────────────────────────── */
/* Текст с многоточием */
.text-truncate-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.text-truncate-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
/* Кастомный scrollbar */
.custom-scrollbar {
&::-webkit-scrollbar {
width: 8px;
height: 8px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--bs-border-color) 70%, transparent);
border-radius: 10px;
&:hover {
background: var(--bs-border-color);
}
}
}

View File

@ -0,0 +1,26 @@
// =============================================================================
// Department Controls (SCSS)
// =============================================================================
// Стили для модального управления отделом
// =============================================================================
// MODAL Z-INDEX OVERRIDES
// =============================================================================
// Модалки поверх всего
.modal-backdrop.show {
z-index: 3000 !important;
}
.modal.show {
z-index: 3010 !important;
}
.modal-dialog {
pointer-events: auto !important;
}
// Портальный dropdown автокомплита ролей
.role-portal {
z-index: 1800 !important;
}

View File

@ -0,0 +1,248 @@
// =============================================================================
// Document List Styles (SCSS)
// =============================================================================
// Стили списка документов, recipient picker, и подтверждений
// =============================================================================
// ПЕРЕМЕННЫЕ
// =============================================================================
$doc-icon-size: 28px;
$doc-row-padding: var(--space-2);
$doc-border-unacked-width: 3px;
$recipient-avatar-size: 26px;
$card-icon-size: 32px;
// Цвета
$doc-hover-primary: 4%;
$doc-unacked-bg: 8%;
$doc-unacked-hover: 15%;
// =============================================================================
// СПИСОК ДОКУМЕНТОВ
// =============================================================================
#docList.card-list {
display: block;
gap: 0 !important;
}
#docList .doc-row {
padding: $doc-row-padding !important;
margin: 0 !important;
border-bottom: 1px solid var(--bs-border-color);
transition: background-color 0.2s ease;
cursor: pointer;
&:hover {
background-color: color-mix(in srgb, var(--bs-primary) $doc-hover-primary, transparent);
}
&.doc-unacked {
background-color: color-mix(in srgb, var(--bs-warning) $doc-unacked-bg, transparent);
border-left: $doc-border-unacked-width solid var(--bs-warning);
padding-left: calc($doc-row-padding - $doc-border-unacked-width) !important;
&:hover {
background-color: color-mix(in srgb, var(--bs-warning) $doc-unacked-hover, transparent);
}
}
&:last-child {
border-bottom: 0;
}
}
// =============================================================================
// ЗАГОЛОВОК ДОКУМЕНТА
// =============================================================================
#docList .card-header {
display: grid;
grid-template-columns: $doc-icon-size 1fr auto;
align-items: center;
gap: 0.75rem;
margin: 0;
padding: 0 !important;
}
#docList .feed-ico {
width: $doc-icon-size;
height: $doc-icon-size;
display: grid;
place-items: center;
border-radius: 8px;
background: color-mix(in srgb, var(--bs-body-bg) 90%, transparent);
border: 1px solid color-mix(in srgb, var(--bs-border-color), transparent 30%);
color: var(--bs-primary);
}
// =============================================================================
// ДЕЙСТВИЯ С ДОКУМЕНТАМИ
// =============================================================================
.doc-actions {
display: flex;
align-items: center;
gap: 0.4rem;
flex-wrap: wrap;
}
// =============================================================================
// SCOPE SWITCH
// =============================================================================
.scope-switch .btn {
min-height: 36px;
border-radius: 999px;
}
// =============================================================================
// АДАПТИВНОСТЬ: МОБИЛЬНЫЕ
// =============================================================================
@media (max-width: 576px) {
#docList .card-header {
grid-template-columns: $doc-icon-size 1fr;
gap: 0.5rem;
}
.doc-actions {
grid-column: 2;
margin-top: 0.5rem;
justify-content: flex-start;
.btn,
.btn-icon {
font-size: 0.85rem;
padding: 0.35rem 0.65rem;
}
}
}
// =============================================================================
// RECIPIENT PICKER
// =============================================================================
.recipient-picker {
border: 1px solid var(--bs-border-color);
border-radius: 12px;
padding: var(--space-2);
background: var(--bs-body-bg);
}
.recipient-picker .rp-selected {
display: flex;
flex-wrap: wrap;
gap: var(--space-1);
margin-bottom: var(--space-1);
}
.recipient-picker .rp-chip {
display: inline-flex;
align-items: center;
gap: var(--space-1);
border: 1px solid var(--bs-border-color);
border-radius: 999px;
padding: var(--space-1);
font-size: 0.85rem;
button {
border: 0;
background: transparent;
padding: 0 var(--space-1);
line-height: 1;
}
}
.recipient-picker .rp-search {
position: relative;
input {
width: 100%;
}
}
.recipient-picker .rp-results {
margin-top: 0.5rem;
max-height: 220px;
overflow: auto;
border: 1px solid var(--bs-border-color);
border-radius: 10px;
}
.recipient-picker .rp-item {
display: flex;
align-items: center;
gap: var(--space-1);
padding: var(--space-1);
border-bottom: 1px solid var(--bs-border-color);
&:last-child {
border-bottom: 0;
}
small {
color: var(--bs-secondary-color);
}
}
.recipient-picker .rp-empty {
padding: var(--space-1);
text-align: center;
color: var(--bs-secondary-color);
}
.recipient-picker .rp-avatar {
width: $recipient-avatar-size;
height: $recipient-avatar-size;
border-radius: 50%;
background: color-mix(in srgb, var(--bs-primary) 12%, transparent);
display: inline-grid;
place-items: center;
font-size: 0.7rem;
}
.recipient-block[hidden] {
display: none !important;
}
// =============================================================================
// ACKNOWLEDGMENTS (ПОДТВЕРЖДЕНИЯ)
// =============================================================================
.acks-list {
max-height: 50vh;
overflow: auto;
margin: 0;
padding-left: 1.1rem;
}
.acks-empty {
color: var(--bs-secondary-color);
}
.acks-more {
margin-top: 0.5rem;
}
// =============================================================================
// ИКОНКИ
// =============================================================================
.card-icon {
width: $card-icon-size;
height: $card-icon-size;
flex: 0 0 $card-icon-size;
border-radius: 50%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.rp-avatar {
width: 100%;
height: 100%;
object-fit: cover;
}

View File

@ -0,0 +1,134 @@
// =============================================================================
// Feed-Specific Styles (SCSS)
// =============================================================================
// Стили, специфичные для постов в ленте новостей
// =============================================================================
// ПЕРЕМЕННЫЕ
// =============================================================================
$feed-pin-bg-light: #fff3cd;
$feed-pin-color-light: #664d03;
$feed-pin-border-light: #ffe69c;
$feed-pin-bg-dark: #3b2e04;
$feed-pin-color-dark: #ffda6a;
$feed-pin-border-dark: #4f3f0a;
$feed-title-color-light: #4b4b4b;
// =============================================================================
// ЗНАЧОК ЗАКРЕПЛЕНИЯ
// =============================================================================
.feed-pin {
margin-left: auto;
font-size: 0.75rem;
padding: var(--space-1, 0.25rem);
border-radius: 999px;
background: $feed-pin-bg-light;
color: $feed-pin-color-light;
border: 1px solid $feed-pin-border-light;
flex-shrink: 0;
[data-bs-theme="dark"] & {
background: $feed-pin-bg-dark;
color: $feed-pin-color-dark;
border-color: $feed-pin-border-dark;
}
}
// =============================================================================
// ЗАГОЛОВОК ПОСТА
// =============================================================================
.feed-title {
font: 700 1.3rem/1.5 var(--bs-body-font-family);
margin: var(--space-1, 0.25rem) 0;
color: $feed-title-color-light;
[data-bs-theme="dark"] & {
color: var(--bs-body-color);
}
}
// =============================================================================
// ТЕКСТ ПОСТА
// =============================================================================
.feed-text {
font-size: 0.95rem;
line-height: 1.6;
color: var(--bs-body-color);
margin: 0;
}
// =============================================================================
// ИЗОБРАЖЕНИЕ ПОСТА
// =============================================================================
.feed-img {
position: relative;
border-top: 1px solid var(--bs-border-color);
background: var(--bs-tertiary-bg);
img {
display: block;
width: 100%;
height: auto;
aspect-ratio: 16/9;
object-fit: cover;
}
}
// =============================================================================
// КНОПКИ ДЕЙСТВИЙ (ЛАЙКИ, КОММЕНТАРИИ)
// =============================================================================
.feed-action {
display: inline-flex;
align-items: center;
gap: var(--space-1, 0.25rem);
padding: var(--space-1, 0.25rem);
border-radius: var(--radius-md, 10px);
background: transparent;
border: none;
color: var(--bs-secondary-color);
font-size: 0.875rem;
cursor: pointer;
transition: background 0.2s ease, color 0.2s ease;
&:hover {
background: var(--bs-tertiary-bg);
color: var(--bs-body-color);
}
&.active {
color: var(--bs-primary);
font-weight: 600;
}
i {
font-size: 1rem;
}
}
// =============================================================================
// ФУТЕР ПОСТА
// =============================================================================
.feed-footer {
padding: var(--space-2, 0.5rem) var(--card-padding, 1rem);
border-top: 1px solid var(--bs-border-color);
font-size: 0.875rem;
color: var(--bs-secondary-color);
}
// =============================================================================
// АДАПТИВНОСТЬ
// =============================================================================
@media (max-width: 576px) {
.feed-title {
font-size: 1.1rem;
}
}

View File

@ -0,0 +1,546 @@
// =============================================================================
// iOS-style Components (SCSS)
// =============================================================================
// iOS-style компоненты: карточки, списки, аккордеоны, badges
//
// Зависимости:
// - variables.css (--feed-radius, --feed-shadow, --navbar-h)
// - common.css (.chip определён там)
// =============================================================================
// ПЕРЕМЕННЫЕ
// =============================================================================
// Карточки
$ios-card-border-radius: var(--feed-radius);
$ios-card-transition: box-shadow 0.18s ease, transform 0.18s ease, border-color 0.18s ease;
$ios-card-hover-transform: translateY(-1px);
// Аватар
$avatar-size: 132px;
$avatar-border-radius: 28px;
$avatar-font-size: 3rem;
// Списки
$ios-list-border-radius: 14px;
$ios-cell-gap: 0.75rem;
$ios-cell-padding: 0.85rem 1rem;
// Badges
$badge-border-radius: 999px;
$badge-font-size: 0.8rem;
$badge-icon-size: 0.95rem;
// Аккордеон
$ios-acc-border-radius: 14px;
// Collapse
$collapse-transition: height 0.35s ease;
$collapse-caret-transition: transform 0.25s ease;
// Контейнер
$container-xxl-max-width: 1820px;
// =============================================================================
// МИКСИНЫ
// =============================================================================
// Миксин для цветовых badge-ов
@mixin badge-variant($color) {
background: color-mix(in srgb, var(--bs-#{$color}) 15%, var(--bs-body-bg));
color: color-mix(in srgb, var(--bs-#{$color}) 60%, black 0%);
border: 1px solid color-mix(in srgb, var(--bs-#{$color}) 35%, transparent);
&:hover {
background: color-mix(in srgb, var(--bs-#{$color}) 20%, var(--bs-body-bg));
}
}
// =============================================================================
// BASE CARD
// =============================================================================
.ios-card {
border: 1px solid color-mix(in srgb, var(--bs-border-color), transparent 10%);
border-radius: $ios-card-border-radius;
background: var(--bs-body-bg);
box-shadow: var(--feed-shadow);
transition: $ios-card-transition;
overflow: hidden;
&:hover {
box-shadow: var(--feed-shadow-hover);
transform: $ios-card-hover-transform;
}
}
// =============================================================================
// HEADER
// =============================================================================
.ios-header {
position: sticky;
top: var(--navbar-h, 0px);
z-index: 5;
padding: var(--space-2) 0;
backdrop-filter: saturate(180%) blur(12px);
background: color-mix(in srgb, var(--bs-body-bg) 85%, transparent);
border-bottom: none !important;
}
.ios-title {
font-weight: 800;
letter-spacing: -0.01em;
}
.row.g-ios {
align-items: flex-start;
}
// =============================================================================
// AVATAR
// =============================================================================
.ios-ava,
.ios-ava--ph {
display: grid;
place-items: center;
background: var(--bs-tertiary-bg);
color: var(--bs-secondary-color);
font-size: $avatar-font-size;
line-height: 1;
width: 100%;
height: 100%;
border-radius: $avatar-border-radius;
border: 1px solid color-mix(in srgb, var(--bs-border-color), transparent 20%);
}
.ava-wrap {
position: relative;
width: $avatar-size;
height: $avatar-size;
margin: 0 auto var(--space-2);
display: block;
}
// =============================================================================
// NAME & ROLE
// =============================================================================
.ios-name {
font-weight: 800;
font-size: 1.25rem;
}
.ios-role {
font-size: 0.9rem;
color: var(--bs-secondary-color);
}
// =============================================================================
// CHIPS
// =============================================================================
// Base .chip определён в common.css
.chips {
display: flex;
flex-wrap: wrap;
gap: var(--space-1);
}
// =============================================================================
// COLLAPSE TRIGGERS
// =============================================================================
.ios-collapse-trigger {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-2);
padding: var(--space-1) var(--space-2);
cursor: pointer;
border-radius: $ios-list-border-radius;
border: 1px solid color-mix(in srgb, var(--bs-border-color), transparent 20%);
background: color-mix(in srgb, var(--bs-body-bg) 94%, transparent);
color: var(--bs-body-color);
}
.ios-collapse-caret {
transition: $collapse-caret-transition;
}
.ios-collapse-trigger[aria-expanded="true"] .ios-collapse-caret {
transform: rotate(180deg);
}
.ios-collapse-trigger + .collapse {
margin-top: 0 !important;
> .ios-list,
> .chips {
margin-top: 0.5rem;
}
}
.collapsing {
overflow: hidden;
transition: $collapse-transition;
}
// =============================================================================
// LISTS
// =============================================================================
.ios-list {
border-radius: $ios-list-border-radius;
overflow: hidden;
background: var(--bs-body-bg);
border: 1px solid color-mix(in srgb, var(--bs-border-color), transparent 10%);
}
.ios-cell {
display: flex;
align-items: center;
gap: $ios-cell-gap;
padding: $ios-cell-padding;
border-bottom: 1px solid color-mix(in srgb, var(--bs-border-color), transparent 10%);
flex-wrap: wrap;
&:last-child {
border-bottom: 0;
}
i {
width: 1.25rem;
text-align: center;
opacity: 0.9;
}
a {
text-decoration: none;
color: inherit;
}
// Departments
&.dept-item {
justify-content: space-between;
> .d-flex {
flex: 0 1 auto;
flex-wrap: wrap;
justify-content: flex-end;
gap: 0.35rem 0.5rem;
max-width: 100%;
}
&.is-editing .dept-role {
display: none !important;
}
}
&.dept-head {
border-left: 0;
background: transparent;
}
}
.ios-section-title {
font-weight: 700;
font-size: 1rem;
margin-bottom: 0.5rem;
}
.rounded-ios {
border-radius: $ios-list-border-radius !important;
}
// =============================================================================
// DEPARTMENTS LIST
// =============================================================================
.dept-line {
display: flex;
align-items: center;
gap: 0.5rem;
min-width: 0;
}
.dept-name {
font-weight: 700;
}
.dept-role {
color: var(--bs-secondary-color);
}
// =============================================================================
// BADGES (HEAD & GEAR)
// =============================================================================
.badge-head,
.badge-gear {
display: inline-flex;
align-items: center;
gap: var(--space-1);
padding: var(--space-1);
border-radius: $badge-border-radius;
font-weight: 700;
font-size: $badge-font-size;
line-height: 1;
white-space: nowrap;
flex: 0 0 auto;
.bi {
font-size: $badge-icon-size;
line-height: 1;
}
}
.badge-head {
@include badge-variant(warning);
}
.badge-gear {
@include badge-variant(primary);
cursor: pointer;
}
// Скрытие текста на мобильных
@media (max-width: 420px) {
.badge-head .lbl,
.badge-gear .lbl {
display: none;
}
}
@media (max-width: 576px) {
.badge-head .lbl,
.badge-gear .lbl {
display: none;
}
}
// =============================================================================
// ACCORDION (IOS-ACC)
// =============================================================================
.ios-acc {
border: 1px solid color-mix(in srgb, var(--bs-border-color), transparent 20%);
border-radius: $ios-acc-border-radius;
background: var(--bs-body-bg);
overflow: hidden;
& + & {
margin-top: var(--space-2);
}
.ios-collapse-trigger {
border: 0;
border-radius: 0;
background: transparent;
width: 100%;
padding: var(--space-2);
&:focus {
box-shadow: none;
}
& + .collapse {
margin-top: 0 !important;
}
}
.collapse {
margin: 0 !important;
}
.ios-list {
border: 0;
border-radius: 0;
}
.chips {
margin: 0;
}
.ios-acc-body {
padding: var(--space-2);
border-top: 1px solid color-mix(in srgb, var(--bs-border-color), transparent 20%);
}
.ios-collapse-trigger + .collapse {
margin-top: 0 !important;
}
.ios-acc-body {
border-top: 0;
}
}
// =============================================================================
// ACTIVITY BLOCK
// =============================================================================
#activity {
.p-4 {
padding: 0 !important;
}
.ios-section-title {
padding: var(--space-2) var(--space-2) var(--space-1) !important;
margin: 0 !important;
}
.activity-feed {
margin: 0 !important;
display: flex;
flex-direction: column;
gap: 1rem;
.card {
width: 100% !important;
max-width: none !important;
border: 0 !important;
box-shadow: none !important;
border-radius: 0 !important;
margin: 0 !important;
}
.card-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.row > * {
flex: 0 0 100% !important;
max-width: 100% !important;
}
}
.card-actions {
border-top: 0 !important;
}
.collapse .bg-body-tertiary.border {
border: 0 !important;
}
}
// =============================================================================
// SKILLS UI
// =============================================================================
#skillsBlock[data-editable="1"] {
.chip.chip-add {
display: inline-flex;
align-items: center;
cursor: pointer;
border-style: dashed;
&.pulse {
transform: scale(0.97);
transition: transform 0.15s ease;
}
.bi {
font-size: 0.95rem;
}
}
.chip.chip-skill:hover {
.lbl {
opacity: 0;
}
.chip-del-btn,
&:focus-within .chip-del-btn {
visibility: visible;
opacity: 1;
}
}
}
.chip.chip-skill {
position: relative;
padding: 0.35rem 0.65rem;
.lbl {
transition: opacity 0.15s ease;
}
.chip-del-btn {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: 0;
padding: 0;
margin: 0;
cursor: pointer;
color: var(--bs-danger);
font-size: 1rem;
line-height: 1;
visibility: hidden;
opacity: 0;
transition: opacity 0.15s ease;
z-index: 1;
.bi {
pointer-events: none;
}
}
}
.skills-empty {
color: var(--bs-secondary-color);
font-size: 0.875rem;
margin-right: 0.25rem;
}
// =============================================================================
// DEPT INLINE EDITOR
// =============================================================================
.dept-edit-btn {
width: 32px;
height: 32px;
padding: 0;
display: grid;
place-items: center;
border-radius: $badge-border-radius;
border: 1px solid color-mix(in srgb, var(--bs-border-color), transparent 30%);
background: color-mix(in srgb, var(--bs-body-bg) 94%, transparent);
&:hover {
background: color-mix(in srgb, var(--bs-body-bg) 88%, transparent);
}
}
.dept-editor-wrap {
flex: 0 0 100%;
width: 100%;
margin-top: 0.5rem;
}
.dept-editor form {
display: flex;
align-items: center;
gap: 0.5rem;
}
.dept-role-select {
min-width: 180px;
}
// =============================================================================
// CONTAINER
// =============================================================================
.container-xxl {
max-width: $container-xxl-max-width;
padding-inline: var(--bs-gutter-x, var(--space-2));
}

View File

@ -0,0 +1,251 @@
// =============================================================================
// iOS-style Search Component (SCSS)
// =============================================================================
// Поле поиска в стиле iOS с иконкой лупы и кнопкой очистки
// Переиспользуемый компонент для различных страниц списков
//
// HTML структура:
// <div class="ios-search">
// <i class="bi bi-search"></i>
// <input type="text" class="ios-search-input" placeholder="Поиск...">
// <button class="ios-search-clear" type="button">
// <i class="bi bi-x-lg"></i>
// </button>
// </div>
// =============================================================================
// ПЕРЕМЕННЫЕ
// =============================================================================
// Размеры (default)
$ios-search-height: 40px;
$ios-search-max-width: 420px;
$ios-search-border-radius: 14px;
$ios-search-font-size: 15px;
// Размеры sm
$ios-search-height-sm: 34px;
$ios-search-border-radius-sm: 12px;
$ios-search-font-size-sm: 14px;
// Размеры lg
$ios-search-height-lg: 48px;
$ios-search-border-radius-lg: 16px;
$ios-search-font-size-lg: 16px;
// Размеры mobile
$ios-search-height-mobile: 38px;
$ios-search-font-size-mobile: 14px;
// Отступы
$ios-search-padding-left: 36px;
$ios-search-padding-right: 36px;
$ios-search-icon-left: 10px;
$ios-search-clear-right: 4px;
$ios-search-padding-left-sm: 32px;
$ios-search-padding-right-sm: 32px;
$ios-search-icon-left-sm: 8px;
$ios-search-clear-right-sm: 3px;
$ios-search-padding-left-lg: 42px;
$ios-search-padding-right-lg: 42px;
$ios-search-icon-left-lg: 12px;
$ios-search-clear-right-lg: 6px;
// Иконки
$ios-search-icon-size: 0.95rem;
$ios-search-icon-size-sm: 0.85rem;
$ios-search-icon-size-lg: 1.1rem;
$ios-search-clear-icon-size: 0.9rem;
// Переходы
$ios-search-transition: box-shadow 0.2s ease, border-color 0.2s ease, background 0.2s ease;
$ios-search-btn-transition: background 0.2s ease;
// =============================================================================
// МИКСИНЫ
// =============================================================================
// Миксин для скрытия нативного крестика браузера
@mixin hide-search-controls {
&::-webkit-search-cancel-button,
&::-webkit-search-decoration {
display: none;
-webkit-appearance: none;
}
&::-ms-clear {
display: none;
}
}
// =============================================================================
// БАЗОВЫЕ СТИЛИ
// =============================================================================
// Контейнер поиска
.ios-search {
position: relative;
width: 100%;
max-width: $ios-search-max-width;
}
// Поле ввода
.ios-search-input {
width: 100%;
height: $ios-search-height;
padding: var(--space-1) $ios-search-padding-right var(--space-1) $ios-search-padding-left;
border-radius: $ios-search-border-radius;
border: 1px solid var(--bs-border-color);
background: color-mix(in srgb, var(--bs-body-bg), #000 4%);
font-size: $ios-search-font-size;
outline: none;
transition: $ios-search-transition;
// Скрываем нативный крестик браузера
@include hide-search-controls;
// Placeholder
&::placeholder {
color: var(--bs-secondary-color);
opacity: 0.6;
}
// Фокус
&:focus {
border-color: var(--bs-primary);
box-shadow: 0 0 0 3px rgba(var(--bs-primary-rgb), 0.15);
background: var(--bs-body-bg);
}
// Темная тема
[data-bs-theme="dark"] & {
background: color-mix(in srgb, var(--bs-body-bg), #fff 6%);
}
}
// =============================================================================
// ИКОНКА ПОИСКА (ЛУПА)
// =============================================================================
.ios-search > .bi-search {
position: absolute;
left: $ios-search-icon-left;
top: 50%;
transform: translateY(-50%);
opacity: 0.55;
pointer-events: none;
font-size: $ios-search-icon-size;
color: var(--bs-body-color);
}
// =============================================================================
// КНОПКА ОЧИСТКИ
// =============================================================================
.ios-search-clear {
position: absolute;
right: $ios-search-clear-right;
top: 50%;
transform: translateY(-50%);
border: 0;
background: transparent;
padding: var(--space-1);
display: none;
cursor: pointer;
border-radius: 50%;
transition: $ios-search-btn-transition;
&:hover {
background: color-mix(in srgb, var(--bs-body-color), transparent 90%);
}
&:active {
background: color-mix(in srgb, var(--bs-body-color), transparent 85%);
}
.bi-x-lg {
opacity: 0.6;
font-size: $ios-search-clear-icon-size;
color: var(--bs-body-color);
}
}
// Показываем кнопку очистки когда есть текст
.ios-search-input:not(:placeholder-shown) ~ .ios-search-clear {
display: block;
}
// =============================================================================
// ВАРИАЦИИ РАЗМЕРОВ
// =============================================================================
// Маленький размер
.ios-search.sm {
.ios-search-input {
height: $ios-search-height-sm;
font-size: $ios-search-font-size-sm;
padding: var(--space-1) $ios-search-padding-right-sm var(--space-1) $ios-search-padding-left-sm;
border-radius: $ios-search-border-radius-sm;
}
> .bi-search {
left: $ios-search-icon-left-sm;
font-size: $ios-search-icon-size-sm;
}
.ios-search-clear {
right: $ios-search-clear-right-sm;
padding: var(--space-1);
}
}
// Большой размер
.ios-search.lg {
.ios-search-input {
height: $ios-search-height-lg;
font-size: $ios-search-font-size-lg;
padding: var(--space-2) $ios-search-padding-right-lg var(--space-2) $ios-search-padding-left-lg;
border-radius: $ios-search-border-radius-lg;
}
> .bi-search {
left: $ios-search-icon-left-lg;
font-size: $ios-search-icon-size-lg;
}
.ios-search-clear {
right: $ios-search-clear-right-lg;
padding: var(--space-1);
}
}
// =============================================================================
// ВАРИАЦИЯ: ТОЛЬКО ИКОНКА БЕЗ КНОПКИ ОЧИСТКИ
// =============================================================================
.ios-search.icon-only {
.ios-search-input {
padding-right: var(--space-3);
}
.ios-search-clear {
display: none !important;
}
}
// =============================================================================
// АДАПТИВНОСТЬ
// =============================================================================
@media (max-width: 576px) {
.ios-search {
max-width: 100%;
.ios-search-input {
height: $ios-search-height-mobile;
font-size: $ios-search-font-size-mobile;
}
}
}

View File

@ -0,0 +1,219 @@
// =============================================================================
// Join Scroller Component (SCSS)
// =============================================================================
// Горизонтальная прокручиваемая лента новых сотрудников с hover-эффектами
// =============================================================================
// ПЕРЕМЕННЫЕ
// =============================================================================
$join-base-size: 56px; // базовый размер аватара
$join-multiplier: 4; // множитель при раскрытии
$join-multiplier-mobile: 3.2; // множитель на мобильных
$join-gap: 0.9rem;
$join-padding: 0.25rem;
$join-transition-duration: 0.22s;
// =============================================================================
// КОНТЕЙНЕР
// =============================================================================
.join-wrap {
padding: var(--space-2);
border: 1px solid color-mix(in srgb, var(--bs-border-color), transparent 10%);
border-radius: var(--feed-radius);
background: var(--bs-body-bg);
box-shadow: var(--feed-shadow);
}
// =============================================================================
// ГОРИЗОНТАЛЬНАЯ ЛЕНТА
// =============================================================================
.join-scroller {
position: relative;
overflow-x: auto;
overflow-y: visible; // для инфо-блока
scrollbar-width: none; // Firefox
-ms-overflow-style: none; // IE/Edge
// Градиентная маска по краям
mask-image: linear-gradient(
to right,
transparent 0%,
black 40px,
black calc(100% - 40px),
transparent 100%
);
-webkit-mask-image: linear-gradient(
to right,
transparent 0%,
black 40px,
black calc(100% - 40px),
transparent 100%
);
&::-webkit-scrollbar {
display: none; // Chrome/Safari
}
}
.join-rail {
display: flex;
flex-wrap: nowrap;
align-items: flex-start;
justify-content: center;
gap: $join-gap;
padding: $join-padding;
min-width: 100%;
}
@media (prefers-reduced-motion: reduce) {
.join-scroller {
scroll-behavior: auto !important;
}
}
// =============================================================================
// ЭЛЕМЕНТ ЛЕНТЫ
// =============================================================================
.join-item {
--base: #{$join-base-size};
--mult: #{$join-multiplier};
--size: var(--base);
position: relative;
display: flex;
flex-direction: column;
align-items: center;
width: var(--size);
flex: 0 0 var(--size);
transition: width $join-transition-duration ease, flex-basis $join-transition-duration ease;
&:hover,
&:focus-within {
--size: calc(var(--base) * var(--mult));
z-index: 2;
}
// Отключение hover при перетаскивании
&.no-hover {
&:hover,
&:focus-within {
--size: var(--base);
z-index: auto;
}
.join-info {
max-height: 0;
opacity: 0;
transform: translateY(-4px);
}
}
}
// =============================================================================
// АВАТАР
// =============================================================================
.join-ava {
width: var(--size);
height: var(--size);
border-radius: 999px;
overflow: hidden;
display: grid;
place-items: center;
border: 1px solid color-mix(in srgb, var(--bs-border-color), transparent 20%);
background: var(--bs-tertiary-bg);
transition: width $join-transition-duration ease,
height $join-transition-duration ease,
box-shadow $join-transition-duration ease,
border-color $join-transition-duration ease;
will-change: width, height;
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
background: var(--bs-tertiary-bg);
}
.bi {
font-size: 1.2rem;
opacity: 0.65;
}
.join-item:hover &,
.join-item:focus-within & {
box-shadow: var(--feed-shadow-hover);
border-color: color-mix(in srgb, var(--bs-border-color), var(--bs-primary) 30%);
}
}
// =============================================================================
// ИНФО-БЛОК
// =============================================================================
.join-info {
width: 100%;
overflow: hidden;
max-height: 0;
opacity: 0;
transform: translateY(-4px);
transition: max-height 0.24s ease, opacity 0.18s ease, transform 0.18s ease;
.join-item:hover &,
.join-item:focus-within & {
max-height: 320px; // с запасом для навыков
opacity: 1;
transform: translateY(0);
}
.name {
font-weight: 800;
line-height: 1.2;
margin-top: 0.5rem;
text-align: center;
}
.sub {
font-size: 0.9rem;
color: var(--bs-secondary-color);
text-align: center;
}
.skills-line {
font-size: 0.9rem;
text-align: center;
margin-top: 0.35rem;
word-break: break-word;
white-space: normal;
b {
font-weight: 800;
}
}
.link {
display: inline-block;
margin-top: 0.35rem;
font-size: 0.9rem;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
// =============================================================================
// АДАПТИВНОСТЬ
// =============================================================================
@media (max-width: 480px) {
.join-item {
--mult: #{$join-multiplier-mobile};
}
}

View File

@ -0,0 +1,42 @@
/**
* layout-spacing.scss
*
* Централизованное управление отступами в app-layout
* Все отступы контролируются через CSS переменные в variables.css
*
* ПРИНЦИП: Контейнеры занимают 100% пространства ячейки (без padding).
* Все отступы между колонками управляются ТОЛЬКО через grid gap.
*
* Переменные для настройки (в variables.css):
* --layout-gap: расстояние между sidebar, main, rightbar (по умолчанию 16px)
* --layout-padding-y: вертикальные отступы layout (по умолчанию 24px)
* --layout-padding-x: горизонтальные отступы layout (по умолчанию 0)
*
* Примеры изменения отступов:
*
* Увеличить расстояние между колонками до 24px:
* :root { --layout-gap: var(--space-3); }
*
* Уменьшить расстояние между колонками до 8px:
* :root { --layout-gap: var(--space-1); }
*
* Уменьшить вертикальные отступы до 16px:
* :root { --layout-padding-y: var(--space-2); }
*
* Убрать вертикальные отступы:
* :root { --layout-padding-y: 0; }
*/
/* =====================================
APP LAYOUT SPACING
===================================== */
// Применяем вертикальные отступы к layout
.app-layout {
padding-top: var(--layout-padding-y);
padding-bottom: var(--layout-padding-y);
}
// Горизонтальные отступы управляются через container-xxl px-2 px-lg-3 в base.html
// Grid gap между колонками управляется через --layout-gap в base-app.scss
// Колонки (sidebar, main, rightbar) НЕ имеют собственных padding - занимают 100% ячейки

View File

@ -0,0 +1,148 @@
// =============================================================================
// List View Component (SCSS)
// =============================================================================
// Универсальные стили для списочных представлений (сотрудники, отделы)
// =============================================================================
// ПЕРЕМЕННЫЕ
// =============================================================================
$list-row-py: var(--space-1); // 8px вертикальные отступы
$list-row-px: var(--space-2); // 16px горизонтальные отступы
$list-gap: var(--space-2); // 16px расстояние внутри строки
$list-avatar-size: 56px; // размер аватара
// =============================================================================
// ТОКЕНЫ
// =============================================================================
.list-view {
--list-row-py: #{$list-row-py};
--list-row-px: #{$list-row-px};
--list-gap: #{$list-gap};
--list-avatar: #{$list-avatar-size};
}
// =============================================================================
// КОНТЕЙНЕР СПИСКОВ
// =============================================================================
.list-view {
background: transparent;
padding: 0;
// Убираем gap от card-list
&.card-list {
display: block;
gap: 0 !important;
}
}
// =============================================================================
// СТРОКИ СПИСКОВ
// =============================================================================
.list-view .list-row {
padding: var(--list-row-py) var(--list-row-px) !important;
margin: 0 !important;
border-bottom: 1px solid var(--bs-border-color);
&:last-of-type {
border-bottom: 0;
}
}
// =============================================================================
// ШАПКА СТРОКИ
// =============================================================================
.list-view .list-row .card-header {
display: flex;
align-items: center;
gap: var(--list-gap);
min-height: var(--list-avatar);
margin: 0;
padding: 0 !important;
}
// Шапка с аватаром: grid layout
.list-view .list-row:has(.card-icon) .card-header {
display: grid;
grid-template-columns: var(--list-avatar) 1fr auto;
}
// =============================================================================
// АВАТАР
// =============================================================================
.list-view .list-row .card-icon {
width: var(--list-avatar);
height: var(--list-avatar);
border-radius: 50%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background: color-mix(in srgb, var(--bs-secondary-bg), transparent 30%);
flex: none;
img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
i {
font-size: 1.5rem;
line-height: 1;
opacity: 0.6;
}
}
// =============================================================================
// КОНТЕНТ
// =============================================================================
.list-view .list-row .card-meta {
margin: 0;
padding: 0;
min-width: 0;
}
.list-view .card-title {
line-height: 1.25;
}
.emp-pos {
color: var(--bs-secondary-color);
}
.list-view .card-subtitle {
margin-top: var(--space-1);
}
// =============================================================================
// ДЕЙСТВИЯ СПРАВА
// =============================================================================
.emp-actions {
justify-self: end;
}
// =============================================================================
// УБИРАЕМ НЕНУЖНЫЕ ОТСТУПЫ
// =============================================================================
.list-view .list-row .card-actions {
padding: 0 !important;
}
.list-view .list-row .card-body {
padding: 0 !important;
&:empty {
display: none !important;
}
}

View File

@ -0,0 +1,182 @@
// =============================================================================
// Logout Modal (iOS-style) (SCSS)
// =============================================================================
// iOS-style модальное окно выхода с анимациями
// =============================================================================
// ПЕРЕМЕННЫЕ
// =============================================================================
$ios-avatar-size: 64px;
$ios-icon-size: 56px;
$ios-btn-min-height: 48px;
$ios-border-radius: 16px;
$ios-border-radius-desktop: 18px;
$ios-grip-width: 36px;
$ios-grip-height: 4px;
// =============================================================================
// OVERLAY
// =============================================================================
.ios-overlay {
position: fixed;
inset: 0;
z-index: 1080;
background: color-mix(in srgb, var(--bs-body-bg), #000 35%);
backdrop-filter: blur(10px) saturate(110%);
display: grid;
place-items: end center; // bottom sheet по умолчанию
@media (min-width: 992px) {
place-items: center; // центрируем на десктопе
}
}
// =============================================================================
// SHEET / CARD
// =============================================================================
.ios-sheet {
width: 100%;
max-width: 560px;
background: color-mix(in srgb, var(--bs-body-bg), #fff 6%);
border: 1px solid color-mix(in srgb, var(--bs-border-color), transparent 30%);
border-radius: $ios-border-radius $ios-border-radius 0 0;
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.28);
animation: slideUp 0.28s ease both;
@media (min-width: 992px) {
border-radius: $ios-border-radius-desktop;
animation: popIn 0.18s ease both;
}
}
// Грип-бар как в iOS
.ios-grip {
width: $ios-grip-width;
height: $ios-grip-height;
border-radius: 999px;
background: color-mix(in srgb, var(--bs-body-color), transparent 70%);
margin: var(--space-1) auto;
}
// =============================================================================
// CONTENT
// =============================================================================
.ios-body {
padding: var(--space-2) var(--space-3) var(--space-3);
@media (min-width: 992px) {
padding: var(--space-3);
}
}
.ios-title {
margin: 0.25rem 0;
text-align: center;
font-weight: 800;
letter-spacing: 0.2px;
}
.ios-sub {
text-align: center;
color: var(--bs-secondary-color);
margin: 0 0 1rem;
}
// =============================================================================
// AVATAR & ICON
// =============================================================================
.ios-ava {
width: $ios-avatar-size;
height: $ios-avatar-size;
border-radius: 999px;
overflow: hidden;
border: 1px solid color-mix(in srgb, var(--bs-border-color), transparent 20%);
background: var(--bs-tertiary-bg);
display: grid;
place-items: center;
margin: 0.25rem auto 0.75rem;
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
}
.ios-ico {
width: $ios-icon-size;
height: $ios-icon-size;
border-radius: 14px;
background: color-mix(in srgb, var(--bs-danger), #000 12%);
color: #fff;
display: grid;
place-items: center;
margin: 0.25rem auto 0.75rem;
}
// =============================================================================
// BUTTONS
// =============================================================================
.ios-actions {
display: grid;
gap: 0.6rem;
}
.ios-btn {
min-height: $ios-btn-min-height;
border-radius: 12px;
font-weight: 700;
&-danger {
background: var(--bs-danger);
border-color: var(--bs-danger);
color: #fff;
&:hover {
filter: brightness(1.02);
}
}
&-ghost {
background: color-mix(in srgb, var(--bs-body-bg), #fff 6%);
border: 1px solid color-mix(in srgb, var(--bs-border-color), transparent 20%);
color: var(--bs-body-color);
&:hover {
background: color-mix(in srgb, var(--bs-body-bg), #fff 9%);
}
}
}
// =============================================================================
// ANIMATIONS
// =============================================================================
@keyframes slideUp {
from {
transform: translateY(16px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@keyframes popIn {
from {
transform: scale(0.98);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}

View File

@ -0,0 +1,29 @@
// =============================================================================
// Modal Overrides (SCSS)
// =============================================================================
// Z-index corrections для модальных окон
// =============================================================================
// MODAL Z-INDEX
// =============================================================================
.modal {
z-index: 1080 !important;
&.show {
z-index: 1080 !important;
}
// Не даём просвечивать через backdrop
.modal-content {
background-color: var(--bs-body-bg);
}
}
.modal-backdrop {
z-index: 1075 !important;
&.show {
z-index: 1075 !important;
}
}

View File

@ -0,0 +1,40 @@
// =============================================================================
// Стили для модальных окон
// =============================================================================
// Скругление углов для всех модальных окон
.modal-content {
border-radius: 1rem !important; // Соответствует карточкам Bootstrap (16px)
}
// Убираем скроллбар у modal-content
.modal-dialog {
.modal-content {
overflow: hidden; // Скрываем скроллбар
}
}
// Специальные стили для модального окна поста
#postDetailModal {
.modal-dialog {
max-width: 800px !important;
width: 100%;
margin: 1.75rem auto;
}
.modal-body {
max-height: 85vh;
overflow-y: auto;
padding: 0;
background: var(--bs-body-bg) !important; // Непрозрачный фон
// Скрываем скроллбар
scrollbar-width: none; // Firefox
-ms-overflow-style: none; // IE и Edge
// Chrome, Safari, Edge
&::-webkit-scrollbar {
display: none;
}
}
}

View File

@ -0,0 +1,262 @@
/**
* navbar.scss
* Стили навбара: меню-тогглер, поиск, уведомления
*/
/* =====================================
NAVBAR BASE
===================================== */
// Navbar overflow visible для dropdown
.app-navbar {
overflow: visible !important;
.container-fluid {
overflow: visible !important;
}
}
/* =====================================
MOBILE NAVBAR HEIGHT
===================================== */
// Увеличенная высота navbar для мобильных устройств
@media (max-width: 767.98px) {
.app-navbar {
min-height: 90px;
.container-fluid {
min-height: 64px;
padding-top: 0.75rem;
padding-bottom: 0.75rem;
}
}
}
/* =====================================
MENU TOGGLE (гамбургер)
===================================== */
// Кнопка «гамбургера» — без круга/рамки
.menu-toggle {
padding: 0;
border: 0;
background: transparent;
line-height: 1;
color: #fff;
&:focus {
outline: none;
box-shadow: none;
}
}
/* =====================================
DROPDOWN AVATAR
===================================== */
// Спрятать стрелочку у dropdown-аватара
.dropdown-toggle.no-caret::after {
display: none !important;
}
/* =====================================
MOBILE SEARCH OVERLAY
===================================== */
.mobile-search-overlay {
position: fixed;
top: var(--navbar-h);
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1030;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
&.active {
opacity: 1;
pointer-events: auto;
}
}
// Светлая тема - забеливание
[data-bs-theme="light"] .mobile-search-overlay {
background-color: rgba(255, 255, 255, 0.85);
}
// Темная тема - затемнение
[data-bs-theme="dark"] .mobile-search-overlay {
background-color: rgba(0, 0, 0, 0.7);
}
/* =====================================
BOTTOM NAVBAR (только для мобильных)
===================================== */
.bottom-navbar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: var(--bs-primary);
border-top: 0.5px solid var(--bs-border-color);
z-index: 1040;
padding: 0;
box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.08);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
// Темная тема
[data-bs-theme="dark"] .bottom-navbar {
background-color: var(--bs-tertiary-bg);
border-top-color: var(--bs-border-color);
box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.3);
}
.bottom-navbar-container {
display: flex;
justify-content: space-around;
align-items: center;
height: 60px;
padding: 0 8px;
max-width: 100%;
}
.bottom-nav-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
height: 100%;
color: rgba(255, 255, 255, 0.7);
text-decoration: none;
border: none;
background: transparent;
padding: 4px 8px;
transition: all 0.2s ease;
position: relative;
cursor: pointer;
&:hover,
&:focus,
&:active {
color: #fff;
background: rgba(255, 255, 255, 0.1);
outline: none;
}
i {
font-size: 1.4rem;
margin-bottom: 2px;
line-height: 1;
}
}
// Темная тема - другой цвет кнопок
[data-bs-theme="dark"] .bottom-nav-item {
color: var(--bs-secondary-color);
&:hover,
&:focus,
&:active {
color: var(--bs-emphasis-color);
background: var(--bs-secondary-bg);
}
}
.bottom-nav-label {
font-size: 0.65rem;
font-weight: 500;
text-align: center;
line-height: 1;
margin-top: 2px;
}
/* =====================================
BOTTOM NAV BADGE
===================================== */
// Иконка с бейджем для уведомлений
.bottom-nav-icon-wrapper {
position: relative;
display: inline-block;
}
.bottom-nav-badge {
position: absolute;
top: -6px;
right: -10px;
background: var(--bs-danger);
color: #fff;
border-radius: 10px; // Telegram использует фиксированный radius для круглых бэйджей
padding: 0 6px;
font-size: 11px;
font-weight: 600;
min-width: 20px;
height: 20px;
display: inline-flex;
align-items: center;
justify-content: center;
text-align: center;
line-height: 1;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); // Более заметная тень
transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275),
background-color 0.2s ease;
z-index: 10;
// Мигание при новом сообщении
&.pulse {
animation: badge-pulse 0.6s ease-out;
}
// Серый для замьюченных чатов
&.badge-muted {
background: var(--bs-secondary);
}
&:empty {
display: none;
}
}
@keyframes badge-pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.15);
}
100% {
transform: scale(1);
}
}
/* =====================================
MOBILE LAYOUT
===================================== */
// Padding для body, чтобы контент не перекрывался нижней панелью
@media (max-width: 767.98px) {
body {
padding-bottom: 60px;
}
// Скрыть bottom navbar на странице чата - композер заменяет навигацию
.chat-container ~ .bottom-navbar,
.chat-page ~ .bottom-navbar,
body:has(.chat-container) .bottom-navbar,
body:has(.chat-page) .bottom-navbar {
display: none;
}
// Убрать padding, когда navbar скрыт
body:has(.chat-container),
body:has(.chat-page) {
padding-bottom: 0;
}
}

View File

@ -0,0 +1,471 @@
// =============================================================================
// Notifications System Styles (SCSS)
// =============================================================================
// Стили для системы уведомлений: колокольчик, dropdown, toast, страница списка
// =============================================================================
// ПЕРЕМЕННЫЕ
// =============================================================================
// Колокольчик
$bell-dot-size: 16px;
$bell-dot-font-size: 9px;
$bell-dot-offset-top: -4px;
$bell-dot-offset-right: -6px;
// Dropdown
$notification-dropdown-width: 360px;
$notification-dropdown-max-height: 500px;
$notification-dropdown-border-radius: 12px;
$notification-dropdown-body-max-height: 300px;
// Элементы уведомлений
$notification-item-padding: 12px 16px;
$notification-icon-size: 36px;
$notification-unread-badge-size: 8px;
// Цвета
$notification-danger: #dc3545;
$notification-primary: #0d6efd;
$notification-unread-bg-light: #f0f7ff;
$notification-unread-bg-dark: #1a3a52;
// Переходы
$notification-transition: background-color 0.2s;
// =============================================================================
// КОЛОКОЛЬЧИК В NAVBAR
// =============================================================================
.bell-wrapper {
position: relative;
display: inline-block;
}
.bell-dot {
position: absolute;
top: $bell-dot-offset-top;
right: $bell-dot-offset-right;
min-width: $bell-dot-size;
height: $bell-dot-size;
padding: 0 4px;
background-color: $notification-danger;
color: white;
border-radius: calc($bell-dot-size / 2);
font-size: $bell-dot-font-size;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.3);
}
// =============================================================================
// DROPDOWN МЕНЮ УВЕДОМЛЕНИЙ
// =============================================================================
.notification-dropdown-menu {
position: fixed;
width: $notification-dropdown-width;
max-height: $notification-dropdown-max-height;
background: white;
border-radius: $notification-dropdown-border-radius;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
z-index: 10000;
overflow: hidden;
display: flex;
flex-direction: column;
[data-bs-theme="dark"] & {
background: #2b3035;
color: #dee2e6;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
}
}
// Заголовок
.notification-dropdown-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #e9ecef;
background: #f8f9fa;
[data-bs-theme="dark"] & {
background: #1f2327;
border-bottom-color: #3d4349;
}
}
// Тело с прокруткой
.notification-dropdown-body {
flex: 1;
overflow-y: auto;
max-height: $notification-dropdown-body-max-height;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
scrollbar-width: thin;
scrollbar-gutter: stable both-edges;
overscroll-behavior: contain;
scrollbar-color: color-mix(in srgb, var(--bs-body-color), transparent 70%) transparent;
&:hover {
scrollbar-color: color-mix(in srgb, var(--bs-body-color), transparent 55%) transparent;
}
// Webkit scrollbar
&::-webkit-scrollbar {
width: 10px;
height: 10px;
}
&::-webkit-scrollbar-track {
background: transparent;
margin-block: 0;
}
&::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--bs-body-color), transparent 75%);
border-radius: 999px;
border: 3px solid transparent;
background-clip: padding-box;
}
&:hover::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--bs-body-color), transparent 55%);
}
// Темная тема scrollbar
[data-bs-theme="dark"] & {
scrollbar-color: color-mix(in srgb, var(--bs-body-color), transparent 78%) transparent;
&:hover {
scrollbar-color: color-mix(in srgb, var(--bs-body-color), transparent 60%) transparent;
}
&::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--bs-body-color), transparent 78%);
}
&:hover::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--bs-body-color), transparent 60%);
}
}
// Скрытие scrollbar на тач-устройствах
@media (hover: none), (pointer: coarse) {
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
}
// Разделитель
.notification-dropdown-divider {
height: 1px;
background: #e9ecef;
margin: 0;
[data-bs-theme="dark"] & {
background: #3d4349;
}
}
// Секция
.notification-dropdown-section {
padding: 8px 0;
}
// =============================================================================
// ЭЛЕМЕНТЫ СПИСКА УВЕДОМЛЕНИЙ
// =============================================================================
.notification-item {
display: flex;
gap: 12px;
padding: $notification-item-padding;
cursor: pointer;
transition: $notification-transition;
position: relative;
border-bottom: 1px solid #e9ecef;
&:last-child {
border-bottom: none;
}
&:hover {
background-color: #f8f9fa;
}
&.unread {
background-color: $notification-unread-bg-light;
&:hover {
background-color: #e6f2ff;
}
}
// Темная тема
[data-bs-theme="dark"] & {
border-bottom-color: #3d4349;
&:hover {
background-color: #343a40;
}
&.unread {
background-color: $notification-unread-bg-dark;
&:hover {
background-color: #1e4260;
}
}
}
}
// Иконка уведомления
.notification-icon {
flex-shrink: 0;
width: $notification-icon-size;
height: $notification-icon-size;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
}
// Контент уведомления
.notification-content {
flex: 1;
min-width: 0;
}
.notification-title {
font-weight: 600;
font-size: 14px;
margin-bottom: 4px;
color: #212529;
[data-bs-theme="dark"] & {
color: #dee2e6;
}
}
.notification-message {
font-size: 13px;
color: #6c757d;
margin-bottom: 4px;
[data-bs-theme="dark"] & {
color: #adb5bd;
}
}
.notification-time {
font-size: 11px;
color: #adb5bd;
[data-bs-theme="dark"] & {
color: #6c757d;
}
}
// Значок непрочитанного
.notification-unread-badge {
flex-shrink: 0;
width: $notification-unread-badge-size;
height: $notification-unread-badge-size;
background-color: $notification-primary;
border-radius: 50%;
margin-left: 8px;
align-self: center;
}
// =============================================================================
// ПУСТОЕ СОСТОЯНИЕ
// =============================================================================
.notification-empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 24px;
color: #adb5bd;
text-align: center;
i {
font-size: 48px;
margin-bottom: 12px;
}
div {
font-size: 14px;
}
[data-bs-theme="dark"] & {
color: #6c757d;
}
}
.extra-small {
font-size: 0.7rem;
}
// =============================================================================
// TOAST УВЕДОМЛЕНИЯ (FALLBACK)
// =============================================================================
.toast-container {
z-index: 9999;
}
.toast {
min-width: 300px;
}
// =============================================================================
// СТРАНИЦА СПИСКА УВЕДОМЛЕНИЙ
// =============================================================================
// Карточки уведомлений - прочитанные
.notification-read {
opacity: 0.7;
.card {
background-color: #f8f9fa;
}
[data-bs-theme="dark"] & {
opacity: 0.65;
.card {
background-color: #1f2327 !important;
border-color: #3d4349 !important;
}
}
}
// Карточки уведомлений - непрочитанные
.notification-unread {
border-left: 4px solid var(--bs-primary);
.card {
background-color: #ffffff;
}
[data-bs-theme="dark"] .card {
background-color: #2b3035;
border-color: #3d4349;
}
}
// Иконки категорий (для страницы списка)
.notification-icon {
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
}
// =============================================================================
// ТЕМНАЯ ТЕМА ДЛЯ СТРАНИЦЫ СПИСКА
// =============================================================================
[data-bs-theme="dark"] {
.card {
background-color: #2b3035;
border-color: #3d4349;
color: #dee2e6;
}
.card-body {
color: #dee2e6;
}
.text-muted {
color: #adb5bd !important;
}
// Кнопки
.btn-outline-primary {
color: #6ea8fe;
border-color: #6ea8fe;
&:hover {
background-color: #0d6efd;
border-color: #0d6efd;
color: #fff;
}
}
.btn-outline-secondary {
color: #adb5bd;
border-color: #6c757d;
&:hover {
background-color: #6c757d;
border-color: #6c757d;
color: #fff;
}
}
.btn-outline-danger {
color: #ea868f;
border-color: #ea868f;
&:hover {
background-color: #dc3545;
border-color: #dc3545;
color: #fff;
}
}
// Формы
.form-select,
.form-control {
background-color: #1f2327;
border-color: #3d4349;
color: #dee2e6;
&:focus {
background-color: #2b3035;
border-color: #6ea8fe;
color: #dee2e6;
}
}
// Пагинация
.pagination {
.page-link {
background-color: #2b3035;
border-color: #3d4349;
color: #6ea8fe;
&:hover {
background-color: #343a40;
border-color: #3d4349;
color: #9ec5fe;
}
}
.page-item.active .page-link {
background-color: #0d6efd;
border-color: #0d6efd;
color: #fff;
}
.page-item.disabled .page-link {
background-color: #1f2327;
border-color: #3d4349;
color: #6c757d;
}
}
}

View File

@ -0,0 +1,477 @@
/**
* _page-header.scss
* Универсальная шапка для страниц списков с:
* - Полупрозрачным фоном (sticky positioning)
* - Иконкой и заголовком
* - Счётчиком элементов
* - Переключателем области (Мои/Все)
* - Кнопкой фильтров
* - Кнопкой создания (вытянутая на десктопе, иконка на мобильных)
* - Поиском iOS-стиль
*/
/* ─────────────────────────────────────────────────────────────
ОСНОВНОЙ КОНТЕЙНЕР ШАПКИ
────────────────────────────────────────────────────────────── */
.page-header {
position: sticky;
top: var(--navbar-h, 60px);
z-index: 10;
padding: var(--space-2) 0;
margin-bottom: var(--space-3);
/* Полупрозрачный фон с градиентом */
background: linear-gradient(
to bottom,
color-mix(in srgb, var(--bs-body-bg) 90%, transparent) 0%,
color-mix(in srgb, var(--bs-body-bg) 85%, transparent) 30%,
color-mix(in srgb, var(--bs-body-bg) 70%, transparent) 50%,
color-mix(in srgb, var(--bs-body-bg) 40%, transparent) 70%,
transparent 100%
);
@include transition((background, box-shadow), $transition-fast);
&::before {
content: '';
@include absolute-cover();
backdrop-filter: saturate(180%) $blur-lg;
-webkit-backdrop-filter: saturate(180%) $blur-lg;
mask-image: linear-gradient(
to bottom,
black 0%,
black 30%,
rgba(0, 0, 0, 0.7) 50%,
rgba(0, 0, 0, 0.4) 70%,
transparent 100%
);
-webkit-mask-image: linear-gradient(
to bottom,
black 0%,
black 30%,
rgba(0, 0, 0, 0.7) 50%,
rgba(0, 0, 0, 0.4) 70%,
transparent 100%
);
pointer-events: none;
z-index: -1;
}
> * {
position: relative;
z-index: $z-base;
}
/* При скролле - более выраженный фон */
&.scrolled {
background: linear-gradient(
to bottom,
color-mix(in srgb, var(--bs-body-bg) 95%, transparent) 0%,
color-mix(in srgb, var(--bs-body-bg) 90%, transparent) 30%,
color-mix(in srgb, var(--bs-body-bg) 75%, transparent) 50%,
color-mix(in srgb, var(--bs-body-bg) 45%, transparent) 70%,
transparent 100%
);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
/* Кнопка создания в виде карточки */
.card {
margin-top: var(--space-2);
width: 100%;
button.dept-row,
a.dept-row {
cursor: pointer;
transition: background-color 0.2s ease;
width: 100%;
padding: var(--space-1) var(--space-2);
text-align: center;
.card-title {
font-weight: 700;
color: var(--bs-body-color);
}
i {
color: var(--bs-primary);
}
&:hover {
background-color: var(--bs-light);
}
}
}
/* Адаптивность */
.d-flex.align-items-center {
flex-wrap: wrap;
}
}
/* ─────────────────────────────────────────────────────────────
CHAT AVATAR WITH TYPE BADGE
────────────────────────────────────────────────────────────── */
.chat-avatar-wrapper {
position: relative;
width: 48px;
height: 48px;
flex-shrink: 0;
}
.chat-avatar {
width: 100%;
height: 100%;
border-radius: 12px;
object-fit: cover;
display: block;
border: 1px solid var(--bs-border-color);
}
.chat-type-badge {
position: absolute;
bottom: -2px;
right: -2px;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--bs-primary);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
border: 2px solid var(--bs-body-bg);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
i {
line-height: 1;
}
[data-bs-theme="dark"] & {
border-color: var(--bs-body-bg);
}
}
/* ─────────────────────────────────────────────────────────────
TOP BAR (заголовок + действия)
────────────────────────────────────────────────────────────── */
.page-header-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-2);
width: 100%;
}
.page-header-title {
display: flex;
align-items: center;
gap: var(--space-2);
flex: 1;
min-width: 0;
h1 {
font-size: 1.5rem;
font-weight: 700;
margin: 0;
color: var(--bs-body-color);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
i {
font-size: 1.75rem;
flex-shrink: 0;
}
}
.page-header-actions {
display: flex;
align-items: center;
gap: var(--space-1);
flex-shrink: 0;
}
/* ─────────────────────────────────────────────────────────────
ЗАГОЛОВОК И СЧЁТЧИК
────────────────────────────────────────────────────────────── */
.page-title {
font-weight: 800;
font-size: 1.75rem;
letter-spacing: -0.01em;
line-height: 1.2;
color: var(--bs-body-color);
flex-shrink: 0;
}
.page-count {
font-size: 0.875rem;
color: var(--bs-secondary-color);
white-space: nowrap;
flex-shrink: 0;
.count-value {
font-weight: 600;
color: var(--bs-body-color);
}
}
/* ─────────────────────────────────────────────────────────────
ПЕРЕКЛЮЧАТЕЛЬ ОБЛАСТИ (МОИ/ВСЕ)
────────────────────────────────────────────────────────────── */
.page-scope-switch {
flex-shrink: 0;
.btn {
min-width: 60px;
font-size: 0.875rem;
border-radius: 999px;
padding: 0.375rem 0.75rem;
transition: all 0.2s ease;
&:first-child {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
&:last-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
&.active {
background-color: var(--bs-primary);
border-color: var(--bs-primary);
color: white;
font-weight: 600;
}
}
}
/* ─────────────────────────────────────────────────────────────
КНОПКА ФИЛЬТРОВ
────────────────────────────────────────────────────────────── */
.page-filter-toggle {
&:hover {
background-color: color-mix(in srgb, var(--bs-primary), transparent 85%);
border-color: var(--bs-primary);
color: var(--bs-primary);
}
&[aria-expanded="true"] {
background-color: var(--bs-primary);
border-color: var(--bs-primary);
color: white;
}
}
/* ─────────────────────────────────────────────────────────────
КНОПКА СОЗДАНИЯ
────────────────────────────────────────────────────────────── */
.page-create-btn {
font-weight: 600;
font-size: 0.875rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease;
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
&:active {
transform: translateY(0);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
}
/* ─────────────────────────────────────────────────────────────
ПОИСК
────────────────────────────────────────────────────────────── */
.page-search {
flex-shrink: 1;
min-width: 200px;
max-width: 400px;
flex-grow: 1;
.ios-search {
width: 100%;
}
}
/* ─────────────────────────────────────────────────────────────
БЛОК ФИЛЬТРОВ
────────────────────────────────────────────────────────────── */
.page-filters-collapse {
margin-top: var(--space-2);
width: 100%;
}
.page-filters {
border-radius: var(--radius-lg, 14px);
border: 1px solid color-mix(in srgb, var(--bs-border-color), transparent 10%);
background: var(--bs-body-bg);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
padding: var(--space-3);
animation: slideDown 0.3s ease;
.btn {
white-space: nowrap;
min-width: fit-content;
}
.d-flex.gap-2 {
flex-wrap: wrap;
}
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* ─────────────────────────────────────────────────────────────
АДАПТИВНОСТЬ
────────────────────────────────────────────────────────────── */
/* Планшеты */
@media (max-width: 991.98px) {
.page-title {
font-size: 1.5rem;
}
.page-header {
padding: var(--space-2) 0;
}
.page-search {
flex-basis: 100%;
max-width: none;
margin-top: var(--space-2);
order: 10;
}
.page-filter-toggle {
order: 11;
margin-top: var(--space-2);
margin-left: auto;
margin-right: 0 !important;
}
}
@media (max-width: 767.98px) {
.page-header-title {
h1 {
font-size: 1.25rem;
}
i {
font-size: 1.5rem;
}
}
}
/* Мобильные */
@media (max-width: 575.98px) {
.page-header {
margin-bottom: var(--space-2);
padding: var(--space-1) 0;
top: var(--navbar-h, 60px);
}
.page-title {
font-size: 1.125rem;
font-weight: 700;
}
.page-count {
font-size: 0.7rem;
}
.page-scope-switch {
gap: 0.25rem;
.btn {
min-width: auto;
font-size: 0.7rem;
padding: 0.2rem 0.4rem;
}
}
.page-filters {
margin-top: 0.5rem;
.btn {
padding: 0.4rem 0.75rem;
font-size: 0.85rem;
}
}
.page-search {
margin-top: 0.5rem;
}
@media (max-width: 480px) {
.page-count {
display: none;
}
.page-header .card {
margin-top: 0;
width: 100%;
button.dept-row,
a.dept-row {
padding: var(--space-1) var(--space-1);
font-size: 0.9rem;
}
}
}
}
/* ─────────────────────────────────────────────────────────────
ТЕМНАЯ ТЕМА
────────────────────────────────────────────────────────────── */
@media (prefers-color-scheme: dark) {
.page-header {
background: linear-gradient(
to bottom,
color-mix(in srgb, var(--bs-body-bg) 85%, transparent) 0%,
color-mix(in srgb, var(--bs-body-bg) 80%, transparent) 45%,
color-mix(in srgb, var(--bs-body-bg) 65%, transparent) 50%,
color-mix(in srgb, var(--bs-body-bg) 35%, transparent) 70%,
transparent 100%
);
border-bottom-color: color-mix(in srgb, var(--bs-border-color), transparent 60%);
&.scrolled {
background: linear-gradient(
to bottom,
color-mix(in srgb, var(--bs-body-bg) 90%, transparent) 0%,
color-mix(in srgb, var(--bs-body-bg) 80%, transparent) 30%,
color-mix(in srgb, var(--bs-body-bg) 72%, transparent) 50%,
color-mix(in srgb, var(--bs-body-bg) 40%, transparent) 70%,
transparent 100%
);
}
.card {
button.dept-row:hover,
a.dept-row:hover {
background-color: color-mix(in srgb, var(--bs-body-bg) 95%, white);
}
}
}
.page-filters {
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3);
}
}

View File

@ -0,0 +1,20 @@
// =============================================================================
// Post Form Component (SCSS)
// =============================================================================
// Стили формы создания/редактирования поста
// =============================================================================
// AUTOGROW TEXTAREA
// =============================================================================
textarea.autogrow {
overflow: hidden; // без скроллов до достижения max-height
resize: none; // убираем ручной ресайз
min-height: 120px;
max-height: 70vh; // не занимаем весь экран
// Когда достигли max-height - включаем скролл
&.is-max {
overflow-y: auto;
}
}

View File

@ -0,0 +1,353 @@
// =============================================================================
// Recipient Picker (Email-style) Styles (SCSS)
// =============================================================================
// Компонент выбора получателей в стиле email-клиента
// =============================================================================
// ПЕРЕМЕННЫЕ
// =============================================================================
$recipient-avatar-size: 32px;
$recipient-avatar-size-mobile: 28px;
$recipient-label-width: 80px;
$recipient-chip-padding: 0.25rem 0.5rem;
$recipient-chip-padding-mobile: 0.2rem 0.4rem;
// Цвета
$chip-primary-color: var(--bs-primary);
$chip-primary-hover: var(--bs-primary-dark, #0056b3);
$chip-secondary-color: var(--bs-secondary);
$chip-secondary-hover: var(--bs-secondary-dark, #5a6268);
// =============================================================================
// ОСНОВНОЙ КОНТЕЙНЕР
// =============================================================================
.recipient-picker-email {
font-size: 0.9375rem;
}
// =============================================================================
// СТРОКИ ПОЛУЧАТЕЛЕЙ (To/CC/Отделы)
// =============================================================================
.recipient-row {
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 0.75rem 0;
border-bottom: 1px solid var(--bs-border-color);
&:last-child {
border-bottom: none;
}
}
// =============================================================================
// МЕТКА (To:/CC:/Отделы:)
// =============================================================================
.recipient-label {
min-width: $recipient-label-width;
padding-top: 0.5rem;
color: var(--bs-secondary);
font-weight: 500;
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0;
i {
font-size: 1.1em;
}
}
// =============================================================================
// ПОЛЕ ВВОДА
// =============================================================================
.recipient-field {
flex: 1;
display: flex;
align-items: flex-start;
gap: 0.5rem;
flex-wrap: wrap;
}
// =============================================================================
// WRAPPER ДЛЯ CHIPS + INPUT
// =============================================================================
.recipient-input-wrapper {
flex: 1;
min-width: 300px;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.5rem;
background: var(--bs-body-bg);
border: 1px solid var(--bs-border-color);
border-radius: var(--bs-border-radius);
position: relative;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
&:focus-within {
border-color: $chip-primary-color;
box-shadow: 0 0 0 0.15rem rgba(var(--bs-primary-rgb), 0.15);
}
}
// =============================================================================
// INPUT ВНУТРИ WRAPPER
// =============================================================================
.recipient-input {
flex: 1;
min-width: 200px;
border: none;
outline: none;
padding: 0.25rem;
font-size: inherit;
background: transparent;
&::placeholder {
color: var(--bs-secondary-color);
opacity: 0.7;
}
}
// =============================================================================
// CHIPS (ВЫБРАННЫЕ ПОЛЬЗОВАТЕЛИ/ОТДЕЛЫ)
// =============================================================================
.recipient-chip {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: $recipient-chip-padding;
background: $chip-primary-color;
color: white;
border-radius: 1rem;
font-size: 0.875rem;
white-space: nowrap;
transition: background-color 0.15s;
&:hover {
background: $chip-primary-hover;
}
&.chip-cc {
background: $chip-secondary-color;
&:hover {
background: $chip-secondary-hover;
}
}
}
.chip-remove {
background: none;
border: none;
padding: 0;
color: white;
cursor: pointer;
display: flex;
align-items: center;
opacity: 0.8;
transition: opacity 0.15s;
&:hover {
opacity: 1;
}
i {
font-size: 1.1em;
line-height: 1;
}
}
// Chips для отделов
.recipient-chips {
display: inline-flex;
flex-wrap: wrap;
gap: 0.375rem;
align-items: center;
}
// =============================================================================
// DROPDOWN С AUTOCOMPLETE
// =============================================================================
.recipient-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
margin-top: 0.25rem;
background: white;
border: 1px solid var(--bs-border-color);
border-radius: var(--bs-border-radius);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
max-height: 300px;
overflow-y: auto;
z-index: 1050;
}
.dropdown-item-user {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.625rem 1rem;
cursor: pointer;
transition: background-color 0.15s;
&:hover {
background-color: var(--bs-light);
}
}
.dropdown-item-empty {
padding: 0.75rem 1rem;
color: var(--bs-secondary);
text-align: center;
font-size: 0.875rem;
}
// =============================================================================
// АВАТАР ПОЛЬЗОВАТЕЛЯ
// =============================================================================
.user-avatar-small {
width: $recipient-avatar-size;
height: $recipient-avatar-size;
border-radius: 50%;
background: $chip-primary-color;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 0.75rem;
flex-shrink: 0;
overflow: hidden;
}
.user-avatar-img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 50%;
}
.user-avatar-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: $chip-primary-color;
color: white;
font-weight: 600;
font-size: 0.75rem;
}
// =============================================================================
// ИНФОРМАЦИЯ О ПОЛЬЗОВАТЕЛЕ
// =============================================================================
.user-info {
flex: 1;
min-width: 0;
}
.user-name {
font-weight: 500;
color: var(--bs-body-color);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.user-subtitle {
font-size: 0.8125rem;
color: var(--bs-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
// =============================================================================
// ПОДСКАЗКИ
// =============================================================================
.alert-sm {
padding: 0.5rem 0.75rem;
margin-top: 0.75rem;
font-size: 0.875rem;
}
// =============================================================================
// АДАПТИВНОСТЬ: ПЛАНШЕТЫ
// =============================================================================
@media (max-width: 768px) {
.recipient-row {
flex-direction: column;
gap: 0.5rem;
}
.recipient-label {
min-width: auto;
padding-top: 0;
}
.recipient-input-wrapper {
min-width: 100%;
}
.recipient-input {
min-width: 150px;
}
}
// =============================================================================
// АДАПТИВНОСТЬ: МОБИЛЬНЫЕ
// =============================================================================
@media (max-width: 576px) {
.recipient-chip {
font-size: 0.8125rem;
padding: $recipient-chip-padding-mobile;
}
.user-avatar-small {
width: $recipient-avatar-size-mobile;
height: $recipient-avatar-size-mobile;
font-size: 0.7rem;
}
.dropdown-item-user {
padding: 0.5rem 0.75rem;
}
}
// =============================================================================
// ТЕМНАЯ ТЕМА
// =============================================================================
@media (prefers-color-scheme: dark) {
.recipient-input-wrapper {
background: var(--bs-dark);
border-color: var(--bs-gray-700);
}
.recipient-dropdown {
background: var(--bs-dark);
border-color: var(--bs-gray-700);
}
.dropdown-item-user:hover {
background-color: var(--bs-gray-800);
}
}

View File

@ -0,0 +1,225 @@
// =============================================================================
// Request Detail Styles (SCSS)
// =============================================================================
// Стили для страницы детального просмотра заявления
// =============================================================================
// ПЕРЕМЕННЫЕ
// =============================================================================
$req-detail-icon-size: 48px;
$req-detail-icon-size-mobile: 40px;
$req-detail-padding: 1rem 1.25rem;
$req-detail-padding-mobile: 0.75rem 1rem;
$req-info-label-width: 140px;
// =============================================================================
// ЗАГОЛОВОК КАРТОЧКИ
// =============================================================================
.request-detail-card .req-detail-header {
padding: $req-detail-padding;
border-bottom: 1px solid var(--bs-border-color);
background: var(--bs-body-bg);
}
.request-detail-card .req-detail-row {
display: flex;
align-items: flex-start;
gap: 1rem;
}
.request-detail-card .req-detail-icon {
flex-shrink: 0;
.card-icon {
width: $req-detail-icon-size;
height: $req-detail-icon-size;
display: flex;
align-items: center;
justify-content: center;
background: var(--bs-tertiary-bg);
border-radius: 0.5rem;
font-size: 1.5rem;
}
}
.request-detail-card .req-detail-content {
flex: 1;
min-width: 0;
h1 {
font-size: 1.25rem;
font-weight: 600;
margin: 0;
line-height: 1.3;
}
}
.request-detail-card .req-detail-actions {
display: flex;
gap: 0.5rem;
flex-shrink: 0;
}
// =============================================================================
// ИНФОРМАЦИОННЫЕ БЛОКИ
// =============================================================================
.req-info-block {
display: flex;
padding: 0.75rem 0;
border-bottom: 1px solid color-mix(in srgb, var(--bs-border-color), transparent 50%);
&:last-child {
border-bottom: none;
padding-bottom: 0;
}
&:first-child {
padding-top: 0;
}
}
.req-info-label {
flex: 0 0 $req-info-label-width;
font-size: 0.875rem;
color: var(--bs-secondary-color);
font-weight: 500;
}
.req-info-value {
flex: 1;
min-width: 0;
font-size: 0.9375rem;
}
.req-comment-text {
padding: 0.75rem 1rem;
background: var(--bs-tertiary-bg);
border-radius: 0.375rem;
line-height: 1.5;
}
// =============================================================================
// СЕКЦИЯ КОММЕНТАРИЕВ
// =============================================================================
#comments-section {
position: sticky;
top: 1rem;
display: flex;
flex-direction: column;
max-height: 80vh;
overflow: visible;
.card-header {
background: var(--bs-body-bg);
padding: 0.75rem 1rem;
font-size: 0.9375rem;
}
.card-body {
flex: 1;
min-height: 100px;
overflow-y: auto;
overflow-x: visible;
}
.list-group-item {
padding: 0.75rem 1rem;
border-left: 0;
border-right: 0;
&:first-child {
border-top: 0;
}
}
.card-footer {
padding: 0.75rem 1rem;
position: relative;
z-index: 100;
overflow: visible;
flex-shrink: 0;
border-top: 1px solid var(--bs-border-color);
.message-field {
background: var(--bs-tertiary-bg);
}
}
.dropdown {
position: static;
}
.dropdown-menu {
z-index: 2000 !important;
position: fixed !important;
max-height: 350px;
overflow-y: auto;
width: auto !important;
min-width: 300px;
}
}
// =============================================================================
// АДАПТИВНОСТЬ: ПЛАНШЕТЫ
// =============================================================================
@media (max-width: 992px) {
.request-detail-card .req-detail-row {
flex-wrap: wrap;
}
.request-detail-card .req-detail-content {
flex: 1 1 calc(100% - 64px);
}
.request-detail-card .req-detail-actions {
width: 100%;
margin-top: 0.75rem;
}
#comments-section {
position: static;
}
.req-info-block {
flex-direction: column;
gap: 0.25rem;
}
.req-info-label {
flex: none;
font-size: 0.75rem;
}
}
// =============================================================================
// АДАПТИВНОСТЬ: МОБИЛЬНЫЕ
// =============================================================================
@media (max-width: 576px) {
.request-detail-card .req-detail-header {
padding: $req-detail-padding-mobile;
}
.request-detail-card .req-detail-icon .card-icon {
width: $req-detail-icon-size-mobile;
height: $req-detail-icon-size-mobile;
font-size: 1.25rem;
}
.request-detail-card .req-detail-content h1 {
font-size: 1.1rem;
}
.request-detail-card .req-detail-actions {
flex-direction: column;
.btn {
width: 100%;
}
}
}

View File

@ -0,0 +1,545 @@
// =============================================================================
// Request List Styles (SCSS)
// =============================================================================
// Табличный вид списка заявлений с адаптивным дизайном
// =============================================================================
// ПЕРЕМЕННЫЕ
// =============================================================================
// Размеры колонок таблицы
$req-icon-size: 48px;
$req-icon-size-tablet: 42px;
$req-icon-size-mobile: 36px;
$req-icon-size-compact: 32px;
// Отступы
$req-row-padding: 0.75rem 1rem;
$req-row-padding-mobile: 1rem 0.75rem;
$req-row-padding-compact: 0.625rem;
$req-cell-gap: 0.75rem;
$req-cell-gap-mobile: 0.75rem;
// Переходы
$req-hover-transition: background-color 0.15s ease;
// =============================================================================
// КОНТЕЙНЕР СПИСКА
// =============================================================================
#reqList.card {
display: block;
gap: 0;
padding: 0;
}
// =============================================================================
// ЗАГОЛОВОК ТАБЛИЦЫ
// =============================================================================
#reqList .req-table-header {
position: sticky;
top: 0;
z-index: 10;
background: var(--bs-body-bg);
border-bottom: 2px solid var(--bs-border-color);
.req-table-row {
padding: 0.625rem 1rem;
}
h6 {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--bs-secondary-color);
margin: 0;
}
.req-cell {
align-items: center;
}
// Выравнивание заголовков
.req-cell-title h6,
.req-cell-author h6 {
text-align: left;
}
.req-cell-dates h6,
.req-cell-status h6,
.req-cell-created h6,
.req-cell-actions h6 {
text-align: center;
}
}
// =============================================================================
// СТРОКА ЗАЯВЛЕНИЯ
// =============================================================================
#reqList .req-row {
padding: 0;
margin: 0;
border-bottom: 1px solid color-mix(in srgb, var(--bs-border-color), transparent 50%);
transition: $req-hover-transition;
&:hover {
background-color: color-mix(in srgb, var(--bs-primary) 3%, transparent);
}
&:last-child {
border-bottom: 0;
}
}
// =============================================================================
// ТАБЛИЧНАЯ СЕТКА
// =============================================================================
#reqList .req-table-row {
display: grid;
grid-template-columns:
$req-icon-size // Иконка
minmax(200px, 2fr) // Заголовок (расширяемая)
minmax(120px, 1fr) // Автор
100px // Даты
110px // Статус
80px // Создано
80px; // Действия
gap: $req-cell-gap;
align-items: start;
padding: $req-row-padding;
}
// =============================================================================
// ЯЧЕЙКИ ТАБЛИЦЫ
// =============================================================================
#reqList .req-cell {
min-width: 0; // Для корректного word-wrap
display: flex;
align-items: center;
}
#reqList .req-cell-content {
width: 100%;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
hyphens: auto;
line-height: 1.4;
}
// =============================================================================
// СПЕЦИФИЧНЫЕ СТИЛИ ЯЧЕЕК
// =============================================================================
#reqList .req-cell-icon {
justify-content: center;
.card-icon {
width: 38px;
height: 38px;
flex-shrink: 0;
}
}
#reqList .req-cell-title {
font-size: 0.9rem;
strong {
display: block;
margin-bottom: 0.125rem;
}
.badge {
font-size: 0.7rem;
padding: 0.2rem 0.4rem;
}
}
#reqList .req-cell-author {
font-size: 0.875rem;
}
#reqList .req-cell-dates {
font-size: 0.8rem;
font-variant-numeric: tabular-nums;
line-height: 1.3;
}
#reqList .req-cell-status {
justify-content: flex-start;
.badge {
font-size: 0.7rem;
padding: 0.25rem 0.5rem;
white-space: nowrap;
}
}
#reqList .req-cell-created {
font-size: 0.75rem;
text-align: center;
}
#reqList .req-cell-actions {
justify-content: center;
gap: 0.25rem;
.req-cell-content {
display: flex;
gap: 0.25rem;
justify-content: center;
}
}
// =============================================================================
// ФУТЕР С КОММЕНТАРИЯМИ
// =============================================================================
#reqList .req-footer {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
margin-left: calc($req-icon-size + $req-cell-gap);
border-top: none;
background: transparent;
}
#reqList .comments-block {
margin: 0;
padding: 1rem;
margin-left: calc($req-icon-size + $req-cell-gap);
}
// =============================================================================
// АДАПТИВНОСТЬ: ПЛАНШЕТЫ
// =============================================================================
@media (max-width: 992px) {
#reqList .req-table-header .req-table-row,
#reqList .req-table-row {
grid-template-columns:
$req-icon-size-tablet // Иконка
minmax(150px, 2fr) // Заголовок
minmax(100px, 1fr) // Автор
90px // Даты
100px // Статус
70px; // Создано
}
// Скрываем колонку действий в заголовке
#reqList .req-table-header .req-cell-actions {
display: none;
}
// Но кнопки остаются видимыми
#reqList .req-cell-actions {
grid-column: 6;
display: flex !important;
justify-content: flex-end;
}
}
// =============================================================================
// АДАПТИВНОСТЬ: МОБИЛЬНЫЕ
// =============================================================================
@media (max-width: 768px) {
// Скрываем заголовок таблицы
#reqList .req-table-header {
display: none;
}
// Переходим на flexbox
#reqList .req-table-row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
padding: $req-row-padding-mobile;
gap: $req-cell-gap-mobile;
align-items: flex-start;
}
// Сброс для ячеек
#reqList .req-cell {
display: block;
min-width: 0;
}
#reqList .req-cell-content {
width: 100%;
}
// Иконка слева сверху
#reqList .req-cell-icon {
flex: 0 0 $req-icon-size-mobile;
padding-top: 0.25rem;
.card-icon {
width: $req-icon-size-compact;
height: $req-icon-size-compact;
}
}
// Заголовок занимает оставшуюся ширину
#reqList .req-cell-title {
flex: 1 1 calc(100% - 48px);
font-size: 0.9rem;
strong {
display: block;
margin-bottom: 0.25rem;
font-size: 0.95rem;
}
.badge {
display: inline-block;
font-size: 0.7rem;
padding: 0.2rem 0.4rem;
}
.small {
display: block;
font-size: 0.8rem;
margin-top: 0.25rem;
}
}
// Остальная информация
#reqList .req-cell-author,
#reqList .req-cell-dates,
#reqList .req-cell-status {
flex: 0 1 auto;
font-size: 0.8rem;
}
#reqList .req-cell-dates {
white-space: nowrap;
br {
display: none;
}
}
#reqList .req-cell-status .badge {
font-size: 0.7rem;
padding: 0.25rem 0.5rem;
white-space: nowrap;
}
// Скрываем дату создания
#reqList .req-cell-created {
display: none;
}
// Кнопки действий на отдельной строке
#reqList .req-cell-actions {
display: flex !important;
flex: 1 1 100%;
gap: 0.5rem;
padding-top: 0.75rem;
border-top: 1px solid var(--bs-border-color-translucent);
margin: 0;
.req-cell-content {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
width: 100%;
}
.btn {
padding: 0.5rem 0.75rem;
font-size: 0.85rem;
}
}
// Футер на полную ширину
#reqList .req-footer {
flex: 1 1 100%;
margin-left: 0;
padding: 0.75rem 0;
border-top: 1px solid var(--bs-border-color-translucent);
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
.btn {
padding: 0.4rem 0.6rem;
font-size: 0.8rem;
}
}
// Блок комментариев
#reqList .comments-block {
margin-left: 0;
padding: 0.75rem;
}
// Строка без hover
#reqList .req-row {
padding: 0;
border-bottom: 1px solid color-mix(in srgb, var(--bs-border-color), transparent 50%);
&:hover {
background-color: transparent;
}
&:last-child {
border-bottom: 0;
}
}
}
// =============================================================================
// КОМПАКТНЫЙ РЕЖИМ
// =============================================================================
@media (max-width: 576px) {
#reqList .req-table-row {
padding: $req-row-padding-compact;
}
#reqList .req-cell-title .small {
display: none; // Скрываем превью
}
#reqList .req-cell-dates {
br {
display: none;
}
.req-cell-content::after {
content: ' — ';
}
}
}
// =============================================================================
// AJAX-РЕЖИМ: REQ-ITEM
// =============================================================================
.req-item {
border-bottom: 1px solid var(--bs-border-color);
padding: 1rem;
transition: background-color 0.2s;
&:hover {
background-color: var(--bs-light);
}
&:last-child {
border-bottom: none;
}
}
.req-header {
margin-bottom: 0.75rem;
}
.req-title-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
margin-bottom: 0.5rem;
}
.req-link {
display: flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
color: var(--bs-body-color);
font-weight: 500;
flex: 1;
min-width: 0;
&:hover {
color: var(--bs-primary);
}
strong {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.req-meta {
display: flex;
flex-wrap: wrap;
gap: 1rem;
font-size: 0.875rem;
color: var(--bs-secondary-color);
> span {
display: flex;
align-items: center;
gap: 0.25rem;
}
}
.req-comment {
margin: 0.5rem 0;
padding: 0.5rem;
background-color: var(--bs-light);
border-radius: 0.375rem;
font-size: 0.875rem;
color: var(--bs-secondary-color);
}
.req-footer {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
align-items: center;
padding-top: 0.75rem;
border-top: 1px solid var(--bs-border-color-translucent);
font-size: 0.875rem;
}
.req-approver,
.req-recipients {
display: flex;
align-items: center;
gap: 0.25rem;
color: var(--bs-secondary-color);
}
// =============================================================================
// BADGE СТАТУСЫ
// =============================================================================
.badge-warning {
background-color: #ffc107;
color: #000;
}
.badge-success {
background-color: #198754;
color: #fff;
}
.badge-danger {
background-color: #dc3545;
color: #fff;
}
.badge-secondary {
background-color: #6c757d;
color: #fff;
}
// =============================================================================
// ВСПОМОГАТЕЛЬНЫЕ
// =============================================================================
.load-more-trigger {
visibility: hidden;
}

View File

@ -0,0 +1,331 @@
/**
* rightbar-calendar.scss
* Стили для компонента правой панели с календарем
* Включает стили для FullCalendar, вертикальной ленты недели и модалов событий
*/
/* =====================================
БАЗА КАЛЕНДАРЯ
===================================== */
.calendar-wrap {
min-height: 360px;
border-radius: 10px;
overflow: hidden;
background: var(--bs-body-bg);
box-shadow: inset 0 1px 3px color-mix(in srgb, var(--bs-body-color), transparent 92%);
border: 0;
}
.rightbar-card {
height: 100%;
display: flex;
flex-direction: column;
border-radius: 16px;
overflow: hidden;
border: 1px solid color-mix(in srgb, var(--bs-border-color), transparent 10%);
background: var(--bs-body-bg);
box-shadow: 0 1px 2px rgba(0,0,0,.04), 0 8px 24px rgba(0,0,0,.08);
min-height: 0;
.card-header {
flex: 0 0 auto;
background: var(--bs-tertiary-bg);
border-bottom: 1px solid var(--bs-border-color);
padding: var(--space-1) var(--space-2);
}
.card-body {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
padding: var(--space-2);
}
}
// Поддержка многострочного header при длинных названиях отделов
.min-w-0 {
min-width: 0;
}
/* =====================================
DESKTOP STICKY LAYOUT
===================================== */
// На десктопе rightbar становится sticky с фиксированной высотой
// Высота = 100vh - navbar - 2*отступ (сверху и снизу)
// top = navbar + отступ сверху
// Не перемещается с основным скролом страницы, скролл только внутри .card-body
@media (min-width: 992px) {
.rightbar {
position: sticky;
top: calc(var(--navbar-h) + var(--layout-padding-y, 12px));
height: calc(100dvh - var(--navbar-h) - 2 * var(--layout-padding-y, 12px));
overflow: hidden;
// padding управляется через Bootstrap классы в шаблоне (px-0, py-3 и т.д.)
}
}
/* =====================================
FULLCALENDAR — КОМПАКТНО
===================================== */
.fc {
font-size: 0.7rem;
--fc-border-color: var(--bs-border-color);
--fc-page-bg-color: transparent;
--fc-neutral-bg-color: transparent;
--fc-today-bg-color: color-mix(in srgb, var(--bs-primary), transparent 85%);
--fc-event-bg-color: var(--bs-primary);
--fc-event-border-color: var(--bs-primary);
--fc-event-text-color: var(--bs-body-bg);
// Убираем границы
.fc-theme-standard .fc-scrollgrid {
border: 0 !important;
}
.fc-theme-standard td,
.fc-theme-standard th {
border: 0 !important;
}
// Toolbar
.fc-toolbar {
padding: var(--space-2);
background: var(--bs-tertiary-bg);
border-bottom: 1px solid var(--bs-border-color);
justify-content: space-between;
}
.fc-toolbar-title {
font-size: 1rem;
font-weight: 600;
color: var(--bs-body-color);
}
.fc-button {
border-radius: 8px;
font-size: .82rem;
padding: var(--space-1);
background: transparent;
border: none;
color: var(--bs-primary);
&:hover {
background: rgba(var(--bs-primary-rgb), .1);
}
}
// Day cells
.fc-daygrid-day-frame {
padding: 2px 3px !important;
}
.fc-daygrid-day-number {
font-size: .82rem;
width: 100%;
height: 1.6rem;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
color: var(--bs-body-color);
text-decoration: none !important;
}
.fc-day-today {
background: none !important;
.fc-daygrid-day-number {
color: var(--bs-primary);
font-weight: 700;
border: 1px solid var(--bs-primary);
border-radius: 50%;
}
}
.fc-day-other .fc-daygrid-day-number {
opacity: .4;
}
// Events
.fc-daygrid-day-events {
padding: 0 2px 2px !important;
row-gap: 2px !important;
}
.fc-daygrid-event-harness {
margin: 0 0 2px 0 !important;
}
.fc-daygrid-event {
border-radius: 6px;
padding: var(--space-1) !important;
line-height: 1.02 !important;
background: color-mix(in srgb, var(--bs-primary), var(--bs-body-bg) 75%);
border: 1px solid color-mix(in srgb, var(--bs-primary), transparent 60%);
color: var(--bs-body-color);
.fc-event-time {
display: none !important;
}
.fc-event-title {
font-size: .62rem !important;
line-height: 1.02 !important;
letter-spacing: -0.2px;
font-weight: 600;
font-family: "Arial Narrow","Roboto Condensed",system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
display: -webkit-box !important;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden !important;
white-space: normal !important;
text-overflow: ellipsis;
max-height: calc(1.02em * 3);
}
}
}
/* =====================================
ВЕРТИКАЛЬНАЯ ЛЕНТА НЕДЕЛИ
===================================== */
.week-strip-header {
margin-top: var(--space-2);
display: flex;
align-items: center;
justify-content: space-between;
color: var(--bs-secondary-color);
font-size: .8rem;
}
.week-vertical {
margin-top: var(--space-1);
display: flex;
flex-direction: column;
gap: var(--space-1);
.week-row {
display: grid;
grid-template-columns: 56px 1fr;
align-items: start;
gap: var(--space-1);
padding: var(--space-1);
border-radius: 12px;
background: color-mix(in srgb, var(--bs-primary), var(--bs-body-bg) 86%);
border: 1px solid color-mix(in srgb, var(--bs-primary), transparent 70%);
&:hover {
background: color-mix(in srgb, var(--bs-primary), var(--bs-body-bg) 82%);
}
}
.content {
.title {
font-size: .8rem;
font-weight: 600;
color: var(--bs-body-color);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
white-space: normal;
}
.meta {
margin-top: .15rem;
font-size: .72rem;
color: var(--bs-secondary-color);
}
}
}
.date-badge {
display: grid;
place-items: center;
border-radius: 10px;
padding: .35rem .25rem;
background: color-mix(in srgb, var(--bs-primary), var(--bs-body-bg) 92%);
border: 1px solid color-mix(in srgb, var(--bs-primary), transparent 75%);
text-align: center;
line-height: 1.05;
.dow {
font-size: .7rem;
font-weight: 700;
color: var(--bs-primary);
text-transform: uppercase;
}
.md {
font-size: .74rem;
font-weight: 700;
color: var(--bs-body-color);
}
}
/* =====================================
OFFCANVAS (МОБАЙЛ)
===================================== */
#rightbarOffcanvas {
height: 100dvh;
height: 100svh;
.offcanvas-body {
display: flex;
flex-direction: column;
gap: var(--space-1);
height: calc(100dvh - var(--rb-offcanvas-head, 56px));
height: calc(100svh - var(--rb-offcanvas-head, 56px));
min-height: 0;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
overscroll-behavior: contain;
}
.calendar-wrap {
flex: 0 0 auto;
}
.week-vertical {
flex: 0 0 auto;
}
}
/* =====================================
COLOR PICKER (PALETTE)
===================================== */
.color-swatch {
width: 26px;
height: 26px;
border: 1px solid var(--bs-border-color);
border-radius: 9999px;
padding: 0;
cursor: pointer;
background: transparent;
&.active {
outline: 2px solid var(--bs-primary);
outline-offset: 2px;
}
}
/* =====================================
EVENT DETAILS MODAL
===================================== */
.color-dot {
width: 12px;
height: 12px;
border-radius: 9999px;
display: inline-block;
vertical-align: middle;
margin-right: 6px;
border: 1px solid #00000022;
}

View File

@ -0,0 +1,198 @@
/**
* _sidebar.scss
* Стили левого сайдбара: навигация, активные элементы, переключатель темы
*/
/* ─────────────────────────────────────────────────────────────
SIDEBAR LAYOUT
────────────────────────────────────────────────────────────── */
/* Offcanvas остаётся как есть (липкий ≥ lg) */
#sidebar {
--bs-offcanvas-border-color: transparent;
--bs-offcanvas-border-width: 0;
--bs-offcanvas-border-radius: 0;
border: 0 !important;
border-radius: 0 !important;
box-shadow: none !important;
background: var(--bs-body-bg);
}
/* На десктопе sidebar становится sticky с фиксированной высотой
Высота = 100vh - navbar - 2*отступ (сверху и снизу)
top = navbar + отступ сверху
Не перемещается с основным скролом страницы, имеет свой внутренний скролл */
@media (min-width: 992px) {
#sidebar.offcanvas-lg {
position: sticky;
top: calc(var(--navbar-h, 60px) + var(--layout-padding-y, 12px));
height: calc(100dvh - var(--navbar-h, 60px) - 2 * var(--layout-padding-y, 12px));
align-self: start;
overflow: auto;
border-right: .5px solid var(--bs-border-color);
transform: none !important;
visibility: visible !important;
}
}
/* ─────────────────────────────────────────────────────────────
NAVIGATION SECTIONS
────────────────────────────────────────────────────────────── */
.side-section {
margin: var(--space-1) 0 var(--space-2);
}
.side-hd {
font-size: .78rem;
font-weight: 800;
letter-spacing: .03em;
text-transform: uppercase;
color: var(--bs-secondary-color);
padding: var(--space-1);
margin: 0 0 var(--space-1) var(--space-1);
}
.side-nav {
display: flex;
flex-direction: column;
gap: .15rem;
}
/* ─────────────────────────────────────────────────────────────
NAV LINKS
────────────────────────────────────────────────────────────── */
.side-nav {
.nav-link {
position: relative;
display: flex;
align-items: center;
gap: .6rem;
padding: .48rem .55rem;
border-radius: $radius-md;
color: var(--bs-body-color);
text-decoration: none;
border: 0;
background: transparent !important;
line-height: 1.15;
@include transition((background, color, transform, box-shadow), $transition-fast);
outline: none;
/* мягкий ховер */
&:hover {
color: var(--bs-primary);
background: color-mix(in srgb, var(--bs-primary) 10%, transparent);
}
/* видимая клавиатурная фокусировка */
&:focus-visible {
@include focus-ring();
}
/* активная «пилюля» + тонкий индикатор слева */
&.active {
color: var(--bs-primary);
font-weight: 800;
background: color-mix(in srgb, var(--bs-primary) 16%, transparent);
&::before {
content: "";
position: absolute;
left: -6px;
top: 8px;
bottom: 8px;
width: 3px;
border-radius: 2px;
background: var(--bs-primary);
}
}
/* «отключённый» пункт выглядит реально disabled */
&.disabled {
opacity: .5;
pointer-events: none;
}
}
}
/* ─────────────────────────────────────────────────────────────
ICONS & BADGES
────────────────────────────────────────────────────────────── */
.side-icon {
width: 1.35rem;
height: 1.35rem;
display: inline-grid;
place-items: center;
flex: 0 0 auto;
> i {
font-size: 1.1rem;
line-height: 1;
}
}
.icon-wrap {
position: relative;
display: inline-grid;
place-items: center;
}
.nav-unread {
position: absolute;
top: -4px;
right: -8px;
min-width: 16px;
height: 16px;
padding: 0 var(--space-1);
border-radius: 9999px;
font-size: 10px;
line-height: 16px;
font-weight: 700;
background: var(--bs-danger);
color: #fff;
border: 1px solid rgba(255, 255, 255, .6);
[data-bs-theme="dark"] & {
border-color: rgba(0, 0, 0, .25);
}
}
/* ─────────────────────────────────────────────────────────────
THEME SWITCHER BUTTON
────────────────────────────────────────────────────────────── */
.btn-theme {
display: inline-flex;
align-items: center;
gap: .5rem;
min-height: 36px;
padding: .35rem .75rem;
border-radius: 999px;
background: var(--bs-tertiary-bg);
color: var(--bs-body-color);
border: 1px solid var(--bs-border-color);
transition: transform .08s ease, background-color .25s ease, border-color .25s ease;
&:hover {
transform: translateY(-1px);
background: color-mix(in srgb, var(--bs-tertiary-bg) 90%, var(--bs-primary) 10%);
border-color: color-mix(in srgb, var(--bs-border-color), var(--bs-primary) 30%);
}
&:focus-visible {
outline: 0;
box-shadow: 0 0 0 2px rgba(var(--bs-primary-rgb), .25);
}
.bi {
font-size: 1rem;
line-height: 1;
}
.mode-label {
font-weight: 700;
font-size: .85rem;
opacity: .9;
@media (max-width: 576px) {
display: none;
}
}
}

View File

@ -0,0 +1,136 @@
/**
* spacing-utils.scss
* Утилиты для точного контроля отступов
*
* Использование CSS переменных из variables.css для унифицированных отступов
*/
/* =====================================
GAP UTILITIES
===================================== */
// Gap для flex/grid контейнеров
.gap-component {
gap: var(--gap-component); // 16px - стандартный gap между элементами
}
.gap-section {
gap: var(--gap-section); // 24px - gap между секциями
}
.gap-1 {
gap: var(--space-1); // 8px
}
.gap-2 {
gap: var(--space-2); // 16px
}
.gap-3 {
gap: var(--space-3); // 24px
}
.gap-4 {
gap: var(--space-4); // 32px
}
/* =====================================
MARGIN UTILITIES
===================================== */
// Margin bottom для секций и блоков
.mb-section {
margin-bottom: var(--margin-section); // 24px - между секциями
}
.mb-block {
margin-bottom: var(--margin-block); // 32px - между блоками
}
// Margin на всех сторонах
.my-section {
margin-block: var(--margin-section); // 24px сверху/снизу
}
.my-block {
margin-block: var(--margin-block); // 32px сверху/снизу
}
/* =====================================
PADDING UTILITIES
===================================== */
// Padding для карточек
.p-card {
padding: var(--card-padding); // 16px со всех сторон
}
.p-card-sm {
padding: var(--card-padding-sm); // 8px со всех сторон
}
.p-card-lg {
padding: var(--card-padding-lg); // 24px со всех сторон
}
// Padding по осям
.px-card {
padding-inline: var(--card-padding); // 16px лево/право
}
.py-card {
padding-block: var(--card-padding); // 16px верх/низ
}
.px-card-sm {
padding-inline: var(--card-padding-sm); // 8px лево/право
}
.py-card-sm {
padding-block: var(--card-padding-sm); // 8px верх/низ
}
.px-card-lg {
padding-inline: var(--card-padding-lg); // 24px лево/право
}
.py-card-lg {
padding-block: var(--card-padding-lg); // 24px верх/низ
}
/* =====================================
RESPONSIVE UTILITIES
===================================== */
// Адаптивный gap для мобильных
@media (max-width: 576px) {
.gap-component-mobile {
gap: var(--space-1); // 8px на мобильных
}
.p-card-mobile {
padding: var(--card-padding-sm); // 8px на мобильных
}
}
/* =====================================
КОМБИНИРОВАННЫЕ КЛАССЫ
===================================== */
// Стандартная секция с отступами
.section-spacing {
margin-bottom: var(--margin-section); // 24px снизу
padding: var(--card-padding); // 16px внутри
}
// Компактная секция
.section-spacing-sm {
margin-bottom: var(--space-2); // 16px снизу
padding: var(--card-padding-sm); // 8px внутри
}
// Просторная секция
.section-spacing-lg {
margin-bottom: var(--margin-block); // 32px снизу
padding: var(--card-padding-lg); // 24px внутри
}

View File

@ -0,0 +1,210 @@
// =============================================================================
// Team Wheel Component (SCSS)
// =============================================================================
// Круговой прокручиваемый виджет сотрудников
// =============================================================================
// ПЕРЕМЕННЫЕ
// =============================================================================
$wheel-size: 200px;
$ava-size: 52px;
$arc-width: 4px;
// =============================================================================
// CSS ПЕРЕМЕННЫЕ
// =============================================================================
:root {
--wheel-size: #{$wheel-size};
--ava-size: #{$ava-size};
--ring-bg: radial-gradient(
ellipse at center,
var(--bs-body-bg),
color-mix(in srgb, var(--bs-body-bg) 85%, var(--bs-secondary-bg, #f7f7f9) 15%)
);
--ring-border: var(--bs-border-color, rgba(0, 0, 0, 0.125));
--shadow-inset: 0 8px 24px rgba(0, 0, 0, 0.06) inset;
--shadow-out: 0 4px 12px rgba(0, 0, 0, 0.05);
--arc-width: #{$arc-width};
--arc-track: color-mix(in srgb, var(--bs-secondary-color) 30%, transparent);
--arc-prog: var(--bs-primary);
}
// =============================================================================
// TEAM WRAP & WHEEL
// =============================================================================
.team-wrap {
width: var(--wheel-size);
display: flex;
flex-direction: column;
align-items: center;
margin-inline: auto;
}
.team-wheel {
position: relative;
width: var(--wheel-size);
height: var(--wheel-size);
border-radius: 50%;
overflow: hidden;
background: var(--ring-bg);
border: 1px solid var(--ring-border);
box-shadow: var(--shadow-inset);
user-select: none;
touch-action: none;
display: flex;
align-items: center;
justify-content: center;
}
// =============================================================================
// WHEEL LAYOUT
// =============================================================================
.wheel-plane {
position: absolute;
inset: 0;
}
.wheel-columns {
position: absolute;
inset: 10px var(--space-1);
display: flex;
gap: var(--space-1);
justify-content: space-between;
align-items: flex-start;
will-change: transform;
}
.wheel-col {
width: calc(25% - 6px);
display: flex;
flex-direction: column;
gap: var(--space-1);
align-items: center;
&.col-2 {
margin-top: calc(var(--ava-size) * 0.5);
}
&.col-3 {
margin-top: calc(var(--ava-size) * 1);
}
&.col-4 {
margin-top: calc(var(--ava-size) * 1.5);
}
}
// =============================================================================
// WHEEL ITEMS
// =============================================================================
.wheel-item {
background: transparent;
border: 0;
padding: 0;
width: var(--ava-size);
height: var(--ava-size);
border-radius: 50%;
cursor: pointer;
box-shadow: var(--shadow-out);
.ava {
width: 100%;
height: 100%;
border-radius: 50%;
overflow: hidden;
display: grid;
place-items: center;
background: var(--bs-tertiary-bg);
border: 1px solid var(--bs-border-color);
img {
width: 100%;
height: 100%;
object-fit: cover;
}
.bi {
font-size: 1.2rem;
opacity: 0.5;
}
}
&:active {
transform: scale(0.96);
}
}
// =============================================================================
// PROGRESS RING
// =============================================================================
.progress-ring {
position: absolute;
inset: -1px;
transform: rotate(-90deg);
pointer-events: none;
}
.progress-ring__circle {
stroke: var(--arc-track);
fill: none;
stroke-width: var(--arc-width);
stroke-linecap: round;
}
.progress-ring__prog {
stroke: var(--arc-prog);
fill: none;
stroke-width: var(--arc-width);
stroke-linecap: round;
transition: stroke-dashoffset 0.35s;
}
// =============================================================================
// CONTROLS
// =============================================================================
.team-controls {
margin-top: var(--space-2);
display: flex;
gap: var(--space-1);
align-items: center;
}
.team-nav {
padding: var(--space-1);
border: 1px solid var(--bs-border-color);
background: var(--bs-body-bg);
border-radius: 8px;
&:active:not(:disabled) {
transform: scale(0.96);
}
&:disabled {
opacity: 0.4;
cursor: not-allowed;
}
}
.team-dots {
display: flex;
gap: 6px;
align-items: center;
}
.team-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: color-mix(in srgb, var(--bs-secondary-color), transparent 70%);
&.active {
background: var(--bs-primary);
}
}

View File

@ -0,0 +1,183 @@
/**
* _chat-date-groups.scss
* Стили для группировки сообщений по датам
* Основано на Telegram Web архитектуре
*/
/* ─────────────────────────────────────────────────────────────
MESSAGE DATE GROUP
────────────────────────────────────────────────────────────── */
.message-date-group {
position: relative;
display: flex;
flex-direction: column;
width: 100%;
}
/* ─────────────────────────────────────────────────────────────
STICKY DATE HEADER
────────────────────────────────────────────────────────────── */
.sticky-date {
position: sticky;
top: 56px;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
padding: 12px 16px;
margin: 0;
font-size: 0.875rem;
font-weight: 500;
color: var(--bs-secondary);
text-align: center;
background-color: transparent;
pointer-events: none;
transition: opacity 0.15s ease, background-color 0.2s ease;
span {
display: inline-block;
padding: 6px 12px;
border-radius: 12px;
background-color: var(--bs-light);
pointer-events: all;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
transition: background-color 0.2s ease, box-shadow 0.2s ease, transform 0.15s ease;
}
&.interactive span {
cursor: pointer;
user-select: none;
&:hover {
background-color: var(--bs-primary);
color: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
transform: translateY(-1px);
}
&:active {
transform: translateY(0);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
&:focus-visible {
outline: 2px solid var(--bs-primary);
outline-offset: 2px;
}
}
&.is-sticky span {
background-color: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
}
/* ─────────────────────────────────────────────────────────────
MESSAGES CONTAINER
────────────────────────────────────────────────────────────── */
.messages-container {
display: flex;
flex-direction: column;
width: 100%;
gap: 0;
}
/* ─────────────────────────────────────────────────────────────
LOADING STATE
────────────────────────────────────────────────────────────── */
.message-date-group.loading .sticky-date span {
animation: pulse 1.5s ease-in-out infinite;
background: linear-gradient(
90deg,
var(--bs-light) 25%,
rgba(0, 0, 0, 0.05) 50%,
var(--bs-light) 75%
);
background-size: 200% 100%;
}
@keyframes pulse {
0%, 100% {
background-position: 200% 0;
}
50% {
background-position: 0 0;
}
}
/* ─────────────────────────────────────────────────────────────
RESPONSIVE DESIGN
────────────────────────────────────────────────────────────── */
@media (max-width: 576px) {
.sticky-date {
top: 48px;
padding: 10px 12px;
font-size: 0.8125rem;
span {
padding: 5px 10px;
border-radius: 10px;
}
}
}
@media (min-width: 577px) and (max-width: 992px) {
.sticky-date {
top: 52px;
}
}
/* ─────────────────────────────────────────────────────────────
DARK MODE SUPPORT
────────────────────────────────────────────────────────────── */
@media (prefers-color-scheme: dark) {
.sticky-date {
color: var(--bs-light);
span {
background-color: rgba(52, 58, 64, 0.95);
color: var(--bs-light);
}
&.interactive span:hover {
background-color: var(--bs-primary);
color: white;
}
&.is-sticky span {
background-color: rgba(52, 58, 64, 0.98);
backdrop-filter: blur(10px);
}
}
}
/* ─────────────────────────────────────────────────────────────
ACCESSIBILITY
────────────────────────────────────────────────────────────── */
@media (prefers-contrast: high) {
.sticky-date span {
border: 1px solid currentColor;
}
}
@media (prefers-reduced-motion: reduce) {
.sticky-date,
.sticky-date span {
transition: none;
}
}
/* ─────────────────────────────────────────────────────────────
PRINT STYLES
────────────────────────────────────────────────────────────── */
@media print {
.sticky-date {
position: static;
page-break-inside: avoid;
&.interactive span {
cursor: default;
}
}
}

View File

@ -0,0 +1,901 @@
/**
* chat-detail.scss
* Стили детального чата: scroll, bubbles, composer
* Layout вынесен в layout/_chat-layout.scss
*/
/* ─────────────────────────────────────────────────────────────
CHAT PAGE CONTAINER
────────────────────────────────────────────────────────────── */
.chat-page {
position: relative;
display: flex;
flex-direction: column;
// Учитываем высоту navbar сверху
height: calc(100vh - var(--navbar-h, 60px));
overflow: hidden;
}
.chat-header {
flex-shrink: 0;
z-index: $z-dropdown - 1;
background: var(--bs-body-bg);
border-bottom: 1px solid var(--bs-border-color);
padding: var(--space-3) var(--space-4);
&-content {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
}
&-title {
display: flex;
align-items: center;
gap: var(--space-3);
flex: 1;
min-width: 0;
h1 {
font-size: 1.25rem;
font-weight: 600;
margin: 0;
color: var(--bs-body-color);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
i {
font-size: 1.5rem;
color: var(--bs-primary);
flex-shrink: 0;
}
}
&-actions {
display: flex;
align-items: center;
gap: var(--space-2);
flex-shrink: 0;
}
}
// Mobile стили для header
@media (max-width: 767.98px) {
.chat-header {
padding: var(--space-2) var(--space-3);
&-title {
gap: var(--space-2);
h1 {
font-size: 1.125rem;
}
i {
font-size: 1.25rem;
}
}
&-actions {
gap: var(--space-1);
}
}
}
/* ─────────────────────────────────────────────────────────────
CHAT CONTAINER (основной контейнер чата)
────────────────────────────────────────────────────────────── */
.chat-container {
// Естественный размер контента, не растягиваем
flex: 1 1 auto;
// Вертикальный flex для: alerts → messages → typing
display: flex;
flex-direction: column;
// Критично для работы overflow в дочерних элементах
min-height: 0;
// Предотвращает переполнение, scroll делегируется .chat-messages-scroll
overflow: hidden;
// Для позиционирования scroll-to-bottom кнопки
position: relative;
// Опциональный background для тестирования layout
// background: var(--bs-body-bg);
// Alerts внутри контейнера не должны сжиматься
> .alert {
flex-shrink: 0;
}
}
/* ─────────────────────────────────────────────────────────────
CHAT MESSAGES SCROLL (основной контейнер сообщений)
────────────────────────────────────────────────────────────── */
.chat-messages-scroll {
flex: 1 0 0;
overflow-y: auto;
overflow-x: hidden;
padding: var(--space-3);
min-height: 0;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
scrollbar-width: thin;
scrollbar-gutter: stable;
overscroll-behavior: contain;
scrollbar-color: color-mix(in srgb, var(--bs-body-color), transparent 70%) transparent;
&:hover {
scrollbar-color: color-mix(in srgb, var(--bs-body-color), transparent 55%) transparent;
}
// Кастомный скроллбар для WebKit
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--bs-body-color), transparent 75%);
border-radius: 4px;
}
&:hover::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--bs-body-color), transparent 55%);
}
}
// Алиас для совместимости
.chat-scroll {
@extend .chat-messages-scroll;
}
.chat-scroll {
@include custom-scrollbar(color-mix(in srgb, var(--bs-body-color), transparent 75%));
&:hover {
scrollbar-color: color-mix(in srgb, var(--bs-body-color), transparent 55%) transparent;
}
}
[data-bs-theme="dark"] .chat-scroll {
scrollbar-color: color-mix(in srgb, var(--bs-body-color), transparent 78%) transparent;
&:hover {
scrollbar-color: color-mix(in srgb, var(--bs-body-color), transparent 60%) transparent;
}
&::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--bs-body-color), transparent 78%);
}
&:hover::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--bs-body-color), transparent 60%);
}
}
@media (hover: none), (pointer: coarse) {
.chat-scroll {
scrollbar-width: none;
}
.chat-scroll::-webkit-scrollbar {
display: none;
}
}
/* ─────────────────────────────────────────────────────────────
CHAT COMPOSER WRAPPER & COMPOSER (Modern Messenger Style)
────────────────────────────────────────────────────────────── */
// TODO: Проблема с пустым пространством под композером на широких экранах
// При изменении ширины окна высота navbar меняется, но calc(100vh - var(--navbar-h))
// не всегда корректно работает. Нужно найти более надежное решение для прижатия
// композера к низу страницы независимо от размера viewport.
.chat-composer-wrapper {
flex-shrink: 0;
margin-top: auto; // Прижимаем к низу chat-page
position: relative;
z-index: $z-base;
background: var(--bs-body-bg);
border-top: 1px solid var(--bs-border-color);
padding: var(--space-2);
padding-bottom: calc(var(--space-2) + env(safe-area-inset-bottom));
}
// Mobile стили для composer - фиксируем внизу экрана (замена bottom navbar)
@media (max-width: 767.98px) {
.chat-composer-wrapper {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: var(--space-2) var(--space-2);
padding-bottom: calc(var(--space-2) + env(safe-area-inset-bottom));
border-top: 1px solid var(--bs-border-color);
background: var(--bs-body-bg);
z-index: 1020; // Над контентом, но под модалами
}
// Уменьшенный padding для мобильных + компенсация высоты фиксированного composer
.chat-messages-scroll {
padding: var(--space-2);
padding-bottom: calc(70px + env(safe-area-inset-bottom)); // Высота composer
}
}
.composer-wrap {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.message-field {
background: color-mix(in srgb, var(--bs-body-bg) 96%, transparent);
border: 1px solid color-mix(in srgb, var(--bs-border-color), transparent 30%);
}
.message-input {
max-height: 180px;
}
// Стили кнопок наследуются из custom-bootstrap.scss (glassmorphism)
.btn-send {
box-shadow: 0 4px 16px rgba(var(--bs-primary-rgb), 0.35);
}
.message-emoji .dropdown-menu {
background: var(--bs-body-bg);
}
/* Превью вложений */
.attachment-preview {
padding: var(--space-1) 0;
max-height: 200px;
overflow-y: auto;
max-width: 100%;
width: 100%;
}
.attachment-item {
background: var(--bs-secondary-bg);
@include transition(background-color, $transition-fast);
max-width: 100%; // Ограничиваем ширину контейнера
}
/* Обрезка текста в форме загрузки файлов */
.attachment-item .flex-grow-1 {
min-width: 0; // Важно для flex-элементов с text-truncate внутри
overflow: hidden; // Предотвращаем выход контента за границы
}
.attachment-item .text-truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
}
/* Вложения в сообщениях */
.bubble .attachment-item {
background: transparent;
padding: 0;
margin-bottom: 8px;
// Медиа-элементы (изображения и видео) - минимальные отступы
&.attachment-item--media {
margin-bottom: 0;
a {
display: block;
padding: 0;
line-height: 0;
}
}
}
// Стили только для ссылок на файлы (документы)
.bubble .attachment-item:not(.attachment-item--media) a {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
border-radius: $radius-sm;
text-decoration: none;
border: none;
@include transition-all($transition-fast);
max-width: 100%;
word-break: break-word;
}
// Attachment links in regular messages (other person)
.bubble-other .attachment-item a {
background: rgba(0, 0, 0, 0.05);
color: inherit;
}
.bubble-other .attachment-item a:hover {
background: rgba(0, 0, 0, 0.08);
}
// Attachment links in own messages
.bubble-me .attachment-item a {
background: rgba(255, 255, 255, 0.15);
color: rgba(255, 255, 255, 0.95);
}
.bubble-me .attachment-item a:hover {
background: rgba(255, 255, 255, 0.25);
}
// Icons in attachments
.bubble .attachment-item a i {
font-size: 1.1rem;
flex-shrink: 0;
}
.bubble-me .attachment-item a i {
color: rgba(255, 255, 255, 0.9);
}
// Text truncation for file names
.bubble .attachment-item a span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0; // Important for flex items
}
.message-attachments video,
.message-attachments img,
.message-attachments audio {
max-width: 100%;
width: auto;
border-radius: $radius-sm;
}
// TODO: Исследовать почему у медиа остаются большие отступы
// Возможные причины: кэш браузера, конфликты с Bootstrap, line-height на родителях
// Попробовать: добавить !important, проверить в DevTools computed styles
// Убираем верхний отступ если вложение - только медиа без текста
.message-attachments:has(.attachment-item--media):not(:has(.attachment-item:not(.attachment-item--media))) {
margin-top: 0.25rem; // Уменьшенный отступ для медиа (вместо mt-2 = 0.5rem)
}
.chat-media {
display: block;
width: auto;
max-width: min(640px, 85vw);
max-height: 520px;
border-radius: $radius-md;
background: rgba(0, 0, 0, 0.08);
content-visibility: auto;
vertical-align: top;
}
// Telegram approach: изображения с aspect-ratio занимают полную ширину контейнера
// Это позволяет браузеру резервировать место до загрузки → нет скачка скролла
.chat-media--image[style*="aspect-ratio"] {
width: 100% !important;
height: auto !important;
}
// Убираем этот конфликтующий селектор - заменяем на более специфичный
.message-attachments .attachment-item:not(.attachment-item--media) > a,
.message-attachments .attachment-item:not(.attachment-item--media) > video,
.message-attachments .attachment-item:not(.attachment-item--media) > img {
display: inline-flex;
max-width: 100%;
}
.chat-media--video {
background: #000;
object-fit: contain;
}
.chat-media--image {
object-fit: cover;
}
.message-pending .chat-media {
min-width: 90px;
min-height: 64px;
}
.message-pending .bubble {
position: relative;
opacity: 0.8;
}
.message-pending .message-status {
color: rgba(255, 255, 255, 0.8);
}
/* Состояния сообщений */
.message-failed {
opacity: 0.6;
.bubble {
border: 1px solid var(--bs-danger);
background: color-mix(in srgb, var(--bs-danger) 10%, transparent);
}
}
.message-highlighted {
animation: messageHighlight 1s ease-in-out;
.bubble {
background: color-mix(in srgb, var(--bs-warning) 20%, var(--bs-secondary-bg)) !important;
}
}
@keyframes messageHighlight {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.02);
}
}
/* Режим выделения сообщений */
.selection-mode {
.msg {
cursor: pointer;
transition: background-color $transition-fast;
&:hover {
background: var(--bs-tertiary-bg);
}
&.selected {
background: color-mix(in srgb, var(--bs-primary) 10%, transparent);
}
}
.msg-select-checkbox {
position: absolute;
left: var(--space-1);
top: 50%;
transform: translateY(-50%);
width: 24px;
height: 24px;
border: 2px solid var(--bs-border-color);
@include radius(50%);
background: var(--bs-body-bg);
cursor: pointer;
transition: all $transition-fast;
@include flex-center();
z-index: 2;
&::after {
content: '\f26b'; // Bootstrap icon checkmark
font-family: 'bootstrap-icons';
color: #fff;
font-size: 12px;
opacity: 0;
transform: scale(0);
transition: all $transition-fast;
}
&.selected {
background: var(--bs-primary);
border-color: var(--bs-primary);
&::after {
opacity: 1;
transform: scale(1);
}
}
}
}
.bubble-other .message-status {
color: rgba(0, 0, 0, 0.5);
}
.message-attachments .text-secondary {
color: inherit;
opacity: 0.7;
}
.bubble-me .message-attachments .text-secondary {
color: rgba(255, 255, 255, 0.9) !important;
opacity: 1;
}
.bubble-me .message-attachments .fw-semibold {
color: rgba(255, 255, 255, 0.95) !important;
}
.bubble-me .message-attachments i {
color: rgba(255, 255, 255, 0.9) !important;
}
.history-loader {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.75rem 0;
color: var(--bs-secondary-color);
}
.history-loader .spinner-border {
width: 1rem;
height: 1rem;
}
/* Выпадающее меню вложений */
.dropdown-menu {
min-width: 200px;
border-radius: $radius-md;
@include shadow($shadow-md);
padding: 8px;
}
.dropdown-item {
border-radius: $radius-sm;
padding: 10px 12px;
@include transition(background-color, $transition-fast);
}
.dropdown-item:hover {
background: var(--bs-secondary-bg);
}
.dropdown-item i {
width: 24px;
}
.composer-tools {
display: flex;
gap: var(--space-1);
align-items: center;
}
/* ─────────────────────────────────────────────────────────────
TYPING INDICATOR
────────────────────────────────────────────────────────────── */
.chat-typing-indicator {
padding: var(--space-2) var(--space-3);
display: flex;
align-items: center;
gap: var(--space-2);
font-size: 0.875rem;
color: var(--bs-secondary-color);
font-style: italic;
background: var(--bs-body-bg);
border-top: 1px solid var(--bs-border-color);
flex-shrink: 0;
&.d-none {
display: none !important;
}
}
.typing-dots {
display: inline-flex;
align-items: center;
gap: 4px;
span {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--bs-secondary-color);
opacity: 0.6;
animation: typingBounce 1.4s infinite both;
&:nth-child(2) {
animation-delay: 0.2s;
}
&:nth-child(3) {
animation-delay: 0.4s;
}
}
}
@keyframes typingBounce {
0%, 60%, 100% {
transform: translateY(0);
}
30% {
transform: translateY(-8px);
}
}
.typing-text {
font-size: 0.875rem;
color: var(--bs-secondary-color);
}
/* ─────────────────────────────────────────────────────────────
HISTORY LOADER
────────────────────────────────────────────────────────────── */
.history-loader {
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
padding: var(--space-3) 0;
color: var(--bs-secondary-color);
font-size: 0.875rem;
.spinner-border {
width: 1rem;
height: 1rem;
border-width: 2px;
}
&.d-none {
display: none !important;
}
}
.initial-loader {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--space-5) 0;
color: var(--bs-secondary-color);
.spinner-border {
width: 3rem;
height: 3rem;
}
p {
margin-top: var(--space-3);
font-size: 1rem;
}
}
/* ─────────────────────────────────────────────────────────────
SCROLL TO BOTTOM BUTTON
────────────────────────────────────────────────────────────── */
.scroll-to-bottom {
position: fixed;
right: var(--space-3);
bottom: calc(var(--space-3) + 80px);
z-index: $z-dropdown;
display: none;
width: 44px;
height: 44px;
@include circle();
padding: 0 !important;
border: none !important;
background: var(--bs-primary) !important;
color: #fff !important;
@include shadow($shadow-lg, rgba(var(--bs-primary-rgb), 0.25));
@include transition((transform, box-shadow), $transition-fast);
display: inline-flex;
align-items: center;
justify-content: center;
opacity: 0;
pointer-events: none;
i {
font-size: 1.25rem;
}
&:hover {
transform: scale(1.08);
box-shadow: 0 6px 20px rgba(var(--bs-primary-rgb), 0.35) !important;
}
&:active {
transform: scale(0.95);
}
}
.scroll-to-bottom.show {
display: inline-flex;
opacity: 1;
pointer-events: auto;
animation: fadeInUp 0.3s ease;
}
/* Badge непрочитанных на кнопке скролла */
.scroll-to-bottom {
position: relative; // Для позиционирования badge
.unread-badge {
position: absolute;
top: -6px;
right: -6px;
min-width: 20px;
height: 20px;
padding: 0 6px;
@include flex-center();
@include radius($badge-radius);
background: var(--bs-danger);
color: #fff;
font-size: 11px;
font-weight: 600;
line-height: 1;
@include shadow($shadow-md);
transition: opacity $transition-fast, transform $transition-fast;
z-index: 1;
// Скрытое состояние
&.badge-hidden {
opacity: 0;
transform: scale(0.5);
pointer-events: none;
}
}
// Когда есть непрочитанные
&.has-unread {
.unread-badge {
opacity: 1;
transform: scale(1);
animation: badgePulse 2s ease-in-out infinite;
}
}
}
@keyframes badgePulse {
0%, 100% {
box-shadow: 0 2px 8px rgba(var(--bs-danger-rgb), 0.3);
}
50% {
box-shadow: 0 2px 12px rgba(var(--bs-danger-rgb), 0.5);
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
// Mobile стили для scroll button
@media (max-width: 767.98px) {
.scroll-to-bottom {
right: var(--space-2);
bottom: calc(var(--space-2) + 70px); // Учитываем composer на мобилке
width: 40px;
height: 40px;
i {
font-size: 1.125rem;
}
.unread-badge {
top: -4px;
right: -4px;
min-width: 18px;
height: 18px;
font-size: 10px;
}
}
}
/* ─────────────────────────────────────────────────────────────
MESSAGES LAYOUT
────────────────────────────────────────────────────────────── */
// Выравнивание аватара по низу сообщения
.d-flex.msg {
align-items: flex-end;
}
/* ─────────────────────────────────────────────────────────────
BUBBLES & AVATARS
────────────────────────────────────────────────────────────── */
.bubble {
border-radius: $radius-lg;
padding: var(--space-1) var(--space-2);
word-break: break-word;
white-space: pre-line; /* Сохраняет \n, но схлопывает пробелы из HTML */
max-width: 70%; /* Ограничиваем ширину bubble */
@include shadow($shadow-xs);
user-select: text; /* Разрешаем выделение текста */
cursor: text; /* Курсор текстового выделения */
// Наследование white-space для вложенных элементов
> div {
white-space: inherit;
}
// Специально для голосований - схлопываем whitespace
.poll-widget,
.poll-question,
.poll-options,
.poll-option,
.poll-footer {
white-space: normal;
}
}
.bubble-me {
background: color-mix(in srgb, var(--bs-primary) 92%, #fff 0%);
color: #fff;
}
.bubble-other {
background: var(--bs-secondary-bg);
color: var(--bs-body-color);
}
.mini-ava {
width: 32px;
height: 32px;
flex-shrink: 0; /* Не сжимать аватар */
border-radius: 50%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background: var(--bs-tertiary-bg);
border: 1px solid var(--bs-border-color);
img {
width: 100%;
height: 100%;
object-fit: cover; /* Кадрируем без деформации */
display: block;
}
i {
font-size: 20px;
color: var(--bs-secondary-color);
}
}
/* ─────────────────────────────────────────────────────────────
DIVIDERS (день / непрочитанные)
────────────────────────────────────────────────────────────── */
.day-divider,
.unread-divider {
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-1);
margin: var(--space-1) 0 var(--space-2);
font-size: 0.85rem;
line-height: 1;
font-weight: 700;
pointer-events: none;
user-select: none;
}
.day-divider::before,
.day-divider::after,
.unread-divider::before,
.unread-divider::after {
content: "";
flex: 1 1 auto;
height: 1px;
background: color-mix(in srgb, var(--bs-border-color), transparent 30%);
}
.day-divider > span,
.unread-divider > span {
flex: 0 0 auto;
padding: 0 var(--space-1);
background: transparent;
}
.day-divider {
color: var(--bs-secondary-color);
}
.unread-divider {
color: var(--bs-danger);
}

View File

@ -0,0 +1,298 @@
/**
* _chat-enhanced.scss
* Enhanced chat features: replies, forwards, reactions, attachments
*/
/* ─────────────────────────────────────────────────────────────
REPLY PREVIEW
────────────────────────────────────────────────────────────── */
.reply-preview {
font-size: 0.85rem;
padding: 4px 8px;
background: rgba(0, 0, 0, 0.05);
border-left: 3px solid var(--bs-primary);
border-radius: $radius-xs;
i {
color: var(--bs-primary);
}
}
/* ─────────────────────────────────────────────────────────────
FORWARD INFO
────────────────────────────────────────────────────────────── */
.forward-info {
font-size: 0.85rem;
color: var(--bs-secondary);
font-style: italic;
i {
color: var(--bs-info);
}
}
/* ─────────────────────────────────────────────────────────────
MESSAGE ACTIONS (shown on hover)
────────────────────────────────────────────────────────────── */
.msg {
position: relative;
&:hover .message-actions {
opacity: 1;
}
&:hover .quick-reactions {
opacity: 1;
pointer-events: all;
}
&.new {
animation: slideIn 0.3s ease-out;
}
&.system-message {
justify-content: center !important;
.bubble {
background: #f8f9fa;
border: 1px dashed #dee2e6;
color: #6c757d;
text-align: center;
font-size: 0.9rem;
font-style: italic;
}
}
.bubble {
em.text-muted {
font-size: 0.9rem;
opacity: 0.7;
}
.bi-pencil-fill {
font-size: 0.7rem;
opacity: 0.6;
}
.bi-pin-angle-fill {
color: var(--bs-warning);
font-size: 1rem;
}
}
}
.message-actions {
position: absolute;
top: -12px;
right: 10px;
background: white;
border: 1px solid #dee2e6;
border-radius: $radius-pill;
padding: 2px 4px;
@include shadow($shadow-sm);
opacity: 0;
@include transition(opacity, $transition-fast);
z-index: 10;
.btn {
padding: 2px 6px;
font-size: 0.85rem;
}
}
/* ─────────────────────────────────────────────────────────────
REACTIONS
────────────────────────────────────────────────────────────── */
.reactions {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.reaction-btn {
border: 1px solid #dee2e6;
background: white;
border-radius: $radius-md;
padding: 2px 8px;
font-size: 0.9rem;
cursor: pointer;
@include transition-all($transition-fast);
&:hover {
background: #f8f9fa;
transform: scale(1.1);
}
&.active {
background: #e3f2fd;
border-color: var(--bs-primary);
}
.count {
font-size: 0.75rem;
color: #666;
margin-left: 2px;
}
}
/* ─────────────────────────────────────────────────────────────
QUICK REACTIONS (on hover)
────────────────────────────────────────────────────────────── */
.quick-reactions {
position: absolute;
bottom: -20px;
left: 50%;
transform: translateX(-50%);
background: white;
border: 1px solid #dee2e6;
border-radius: $radius-pill;
padding: 4px 8px;
@include shadow($shadow-md);
opacity: 0;
pointer-events: none;
@include transition(opacity, $transition-fast);
z-index: 20;
display: flex;
gap: 4px;
button {
border: none;
background: transparent;
font-size: 1.1rem;
padding: 4px 8px;
cursor: pointer;
border-radius: $radius-sm;
@include transition(background, $transition-fast);
&:hover {
background: #f0f0f0;
}
}
}
/* ─────────────────────────────────────────────────────────────
ATTACHMENTS
────────────────────────────────────────────────────────────── */
.attachments {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.attachment-image {
max-width: 200px;
max-height: 200px;
border-radius: $radius-sm;
cursor: pointer;
@include transition(transform, $transition-fast);
&:hover {
transform: scale(1.05);
}
}
.attachment-file {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: rgba(0, 0, 0, 0.05);
border-radius: $radius-sm;
text-decoration: none;
color: inherit;
font-size: 0.9rem;
&:hover {
background: rgba(0, 0, 0, 0.1);
}
}
/* ─────────────────────────────────────────────────────────────
CHAT LIST ITEMS
────────────────────────────────────────────────────────────── */
.chat-list-item {
position: relative;
&.pinned {
background: #fffbeb;
border-left: 4px solid var(--bs-warning);
}
}
/* ─────────────────────────────────────────────────────────────
BADGES & INDICATORS
────────────────────────────────────────────────────────────── */
.mention-badge {
background: #ef4444;
color: white;
border-radius: 12px;
padding: 2px 8px;
font-size: 0.75rem;
font-weight: bold;
}
.thread-indicator {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 0.85rem;
color: var(--bs-primary);
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
/* ─────────────────────────────────────────────────────────────
INDICATORS
────────────────────────────────────────────────────────────── */
#typing {
padding: 4px 12px;
font-style: italic;
}
#editing-indicator,
#reply-indicator {
margin-bottom: 8px;
border-radius: 8px;
}
/* ─────────────────────────────────────────────────────────────
ANIMATIONS
────────────────────────────────────────────────────────────── */
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* ─────────────────────────────────────────────────────────────
MOBILE RESPONSIVE
────────────────────────────────────────────────────────────── */
@media (max-width: 768px) {
.message-actions {
position: static;
opacity: 1;
margin-top: 8px;
}
.quick-reactions {
position: static;
transform: none;
opacity: 1;
pointer-events: all;
margin-top: 8px;
justify-content: center;
}
.attachment-image {
max-width: 150px;
max-height: 150px;
}
}

View File

@ -0,0 +1,320 @@
/**
* _chat-list-enhanced.scss
* Enhanced chat list with context menus, animations, avatars
*/
/* ─────────────────────────────────────────────────────────────
CONTEXT MENU
────────────────────────────────────────────────────────────── */
.context-menu {
position: absolute;
background: white;
border: 1px solid #dee2e6;
border-radius: $radius-sm;
@include shadow($shadow-md);
padding: 4px 0;
min-width: 200px;
z-index: 9999;
button {
width: 100%;
border: none;
background: transparent;
padding: 8px 16px;
text-align: left;
cursor: pointer;
font-size: 0.9rem;
display: flex;
align-items: center;
gap: 8px;
&:hover {
background: #f8f9fa;
}
}
hr {
margin: 4px 0;
border-color: #dee2e6;
}
}
/* ─────────────────────────────────────────────────────────────
CHAT ROW
────────────────────────────────────────────────────────────── */
.chat-row {
border-top: 1px solid var(--bs-border-color);
overflow: hidden;
min-height: 60px;
&.updated {
background: #e3f2fd;
@include transition(background, $transition-medium);
}
&.pinned {
background: #fffbeb;
border-left: 4px solid var(--bs-warning);
}
.flex-grow-1 {
min-width: 0;
overflow: hidden;
a,
.card-title {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}
}
[data-last-preview],
.card-subtitle {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}
&:hover .chat-actions-menu .btn {
opacity: 1;
}
}
/* ─────────────────────────────────────────────────────────────
CHAT SECTION
────────────────────────────────────────────────────────────── */
.chat-section {
overflow: visible !important;
position: relative;
z-index: 1;
&:has(.dropdown-menu.show) {
z-index: 1000;
}
.card-header {
text-align: center;
border-bottom: 1px solid var(--bs-border-color);
.card-icon {
display: none;
}
.card-meta {
width: 100%;
text-align: center;
}
}
.list-chats {
overflow: visible !important;
}
}
/* ─────────────────────────────────────────────────────────────
CHAT AVATARS - КРУГЛЫЕ!
────────────────────────────────────────────────────────────── */
.chat-avatar,
.card-icon img,
.chat-list-wrapper .card-icon,
.chat-row .card-icon,
.chat-section .card-icon {
border-radius: 50% !important;
}
.chat-avatar,
.card-icon img {
width: 44px !important;
height: 44px !important;
object-fit: cover;
border: 2px solid var(--bs-border-color);
}
.chat-list-wrapper .card-icon,
.chat-row .card-icon {
width: 44px !important;
height: 44px !important;
min-width: 44px;
min-height: 44px;
border-radius: 50% !important;
overflow: hidden;
}
.chat-avatar-group {
display: flex;
align-items: center;
img {
margin-left: -8px;
border: 2px solid white;
border-radius: 50% !important;
&:first-child {
margin-left: 0;
}
}
}
/* ─────────────────────────────────────────────────────────────
CHAT ACTIONS MENU
────────────────────────────────────────────────────────────── */
.chat-actions-menu {
display: flex;
align-items: center;
flex-shrink: 0;
.btn {
opacity: 0;
@include transition(opacity, $transition-fast);
&:focus,
&[aria-expanded="true"] {
opacity: 1;
}
}
.dropdown-menu {
font-size: 0.9rem;
min-width: 200px;
z-index: 9999;
position: fixed !important;
}
.dropdown-item {
padding: 0.5rem 1rem;
i {
width: 20px;
text-align: center;
}
}
}
/* ─────────────────────────────────────────────────────────────
CHAT TYPE SIDEBAR
────────────────────────────────────────────────────────────── */
:root {
--chat-sidebar-offset: 120px;
}
.chat-type-sidebar {
flex-shrink: 0;
align-self: flex-start;
position: sticky;
top: calc(var(--navbar-h, 60px) + var(--chat-sidebar-offset, 120px));
.card {
max-height: calc(100vh - var(--navbar-h, 60px) - var(--chat-sidebar-offset, 120px) - 24px);
overflow-y: auto;
}
}
.chat-type-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.35rem;
position: relative;
cursor: pointer;
@include transition((background-color, opacity), $transition-fast);
&[style*="display: none"] {
display: none !important;
}
&:hover {
background-color: var(--bs-light) !important;
}
&.active {
background-color: var(--bs-primary) !important;
color: white !important;
i {
color: white;
}
.badge {
background-color: rgba(255, 255, 255, 0.3) !important;
color: white !important;
border-color: white !important;
}
}
.badge {
position: absolute;
top: 0.25rem;
right: 0.25rem;
min-width: 1.25rem;
height: 1.25rem;
padding: 0.15rem 0.3rem;
border-radius: 50%;
font-size: 0.65rem;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
}
}
/* ─────────────────────────────────────────────────────────────
MOBILE VERSION
────────────────────────────────────────────────────────────── */
@media (max-width: 768px) {
.chat-list-wrapper {
flex-direction: column !important;
}
.chat-type-sidebar {
width: 100% !important;
order: -1;
position: static;
.card {
max-height: none;
overflow: hidden;
padding: 0.25rem 0.5rem;
width: 100%;
}
.list-group {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
gap: 0.5rem;
width: 100%;
overflow-x: auto;
overflow-y: hidden;
padding: 0 0.25rem 0.25rem;
scrollbar-width: thin;
scroll-snap-type: x proximity;
-webkit-overflow-scrolling: touch;
}
}
.chat-type-btn {
flex: 0 0 auto;
min-width: 60px;
max-width: 80px;
padding: 0.5rem 0.25rem !important;
border-radius: 0.75rem !important;
border: 1px solid var(--bs-border-color) !important;
scroll-snap-align: start;
i {
font-size: 1.25rem !important;
}
.badge {
font-size: 0.6rem;
min-width: 1.1rem;
height: 1.1rem;
}
}
}

View File

@ -0,0 +1,149 @@
/**
* _chat-new-messages-indicator.scss
* Индикатор новых сообщений в чате
* Кнопка для прокрутки к новым сообщениям
*/
/* ─────────────────────────────────────────────────────────────
NEW MESSAGES INDICATOR
────────────────────────────────────────────────────────────── */
.new-messages-indicator {
position: absolute;
bottom: 20px;
right: 20px;
z-index: 1000;
padding: 10px 16px;
min-width: 180px;
background: var(--bs-primary, #0d6efd);
color: white;
border: none;
border-radius: 24px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
font-size: 14px;
font-weight: 500;
opacity: 0;
transform: translateY(10px);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
display: none;
align-items: center;
justify-content: center;
gap: 8px;
cursor: pointer;
&.show {
opacity: 1;
transform: translateY(0);
pointer-events: all;
animation: pulse 0.6s ease-in-out;
}
&:hover {
background: var(--bs-primary-dark, #0a58ca);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
transform: translateY(-2px);
}
&:active {
transform: translateY(0) scale(0.98);
}
svg {
width: 20px;
height: 20px;
flex-shrink: 0;
}
.badge {
display: inline-block;
min-width: 20px;
padding: 2px 6px;
font-size: 12px;
font-weight: 600;
line-height: 1;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 10px;
background-color: rgba(255, 255, 255, 0.3);
color: white;
margin: 0 4px;
}
/* Варианты расположения */
&.left {
right: auto;
left: 20px;
}
&.center {
left: 50%;
right: auto;
transform: translateX(-50%) translateY(10px);
&.show {
transform: translateX(-50%) translateY(0);
}
&:hover {
transform: translateX(-50%) translateY(-2px);
}
}
}
/* ─────────────────────────────────────────────────────────────
CHAT SCROLL CONTAINER
────────────────────────────────────────────────────────────── */
#chatScroll {
position: relative;
}
/* ─────────────────────────────────────────────────────────────
ANIMATIONS
────────────────────────────────────────────────────────────── */
@keyframes pulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
}
/* ─────────────────────────────────────────────────────────────
RESPONSIVE
────────────────────────────────────────────────────────────── */
@media (max-width: 768px) {
.new-messages-indicator {
bottom: 15px;
right: 15px;
min-width: 160px;
padding: 8px 14px;
font-size: 13px;
svg {
width: 18px;
height: 18px;
}
.badge {
font-size: 11px;
min-width: 18px;
padding: 2px 5px;
}
}
}
/* ─────────────────────────────────────────────────────────────
DARK THEME
────────────────────────────────────────────────────────────── */
@media (prefers-color-scheme: dark) {
.new-messages-indicator {
background: #1a73e8;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
&:hover {
background: #1557b0;
}
}
}

View File

@ -0,0 +1,187 @@
/**
* _chat-polls.scss
* Стили для голосований в чате
*/
/* ─────────────────────────────────────────────────────────────
POLL MODAL
────────────────────────────────────────────────────────────── */
#pollModal .modal-dialog {
display: flex;
align-items: center;
min-height: calc(100% - 1rem);
@media (min-width: 576px) {
min-height: calc(100% - 3.5rem);
max-width: 500px;
margin: 1.75rem auto;
}
}
#pollModal .remove-option {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
#pollOptions .input-group-text {
min-width: 40px;
justify-content: center;
font-weight: 600;
}
/* ─────────────────────────────────────────────────────────────
POLL WIDGET
────────────────────────────────────────────────────────────── */
.poll-widget {
padding: 1rem;
background: var(--bs-secondary-bg);
border-radius: $radius-sm;
margin: 0.5rem 0;
}
.poll-question {
font-size: 1.1rem;
color: var(--bs-body-color);
font-weight: 400;
}
.poll-option {
position: relative;
&.voted {
background: rgba(var(--bs-success-rgb), 0.1);
padding: 0.5rem;
border-radius: $radius-xs;
.progress-bar {
background-color: var(--bs-success);
}
}
}
.btn-poll-option {
@include transition-all($transition-fast);
&:hover {
background: var(--bs-primary);
color: white;
border-color: var(--bs-primary);
transform: translateX(4px);
}
}
.poll-option-result {
.progress {
background-color: rgba(0, 0, 0, 0.1);
}
.progress-bar {
background-color: var(--bs-primary);
@include transition(width, $transition-slow);
}
}
.poll-footer {
border-top: 1px solid rgba(0, 0, 0, 0.1);
padding-top: 0.75rem;
}
/* ─────────────────────────────────────────────────────────────
POLL IN MESSAGE BUBBLES
────────────────────────────────────────────────────────────── */
.message-bubble .poll-widget {
max-width: 500px;
}
/* Голосования в сообщениях других пользователей */
.bubble-other .poll-widget {
background: var(--bs-secondary-bg);
}
/* Голосования в своих сообщениях */
.bubble-me {
.poll-widget {
background: rgba(255, 255, 255, 0.15);
}
.poll-question {
color: #fff;
}
.poll-option-result {
.progress {
background-color: rgba(255, 255, 255, 0.2);
}
.progress-bar {
background-color: rgba(255, 255, 255, 0.6);
}
}
.poll-footer {
border-top-color: rgba(255, 255, 255, 0.2);
}
.poll-voters-list {
background: rgba(255, 255, 255, 0.1);
}
}
/* ─────────────────────────────────────────────────────────────
POLL VOTERS
────────────────────────────────────────────────────────────── */
.poll-voters {
margin-top: 0.5rem;
}
.poll-voters-toggle {
cursor: pointer;
transition: color 0.2s;
&:hover {
color: var(--bs-primary) !important;
text-decoration: underline !important;
}
i {
transition: transform 0.2s;
font-size: 0.75rem;
}
}
.poll-voters-list {
animation: slideDown 0.2s ease-out;
background: rgba(0, 0, 0, 0.03);
padding: 0.5rem 1rem;
border-radius: 0.25rem;
margin-top: 0.5rem;
[data-bs-theme="dark"] & {
background: rgba(255, 255, 255, 0.05);
}
}
.voter-item {
padding: 0.25rem 0;
display: flex;
align-items: center;
i {
opacity: 0.6;
font-size: 0.875rem;
}
}
/* ─────────────────────────────────────────────────────────────
ANIMATIONS
────────────────────────────────────────────────────────────── */
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

View File

@ -0,0 +1,343 @@
/**
* _message-context-menu.scss
* Контекстное меню сообщений (long-press / right-click)
*/
/* ─────────────────────────────────────────────────────────────
MESSAGE INTERACTIVE
────────────────────────────────────────────────────────────── */
[data-message-id] {
cursor: pointer;
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none;
position: relative;
}
/* ─────────────────────────────────────────────────────────────
CONTEXT MENU
────────────────────────────────────────────────────────────── */
.message-context-menu {
position: fixed;
z-index: 9999;
background: #ffffff; // Явно белый для светлой темы
border-radius: var(--radius-lg);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
min-width: 280px;
max-width: 320px;
opacity: 0;
transform: scale(0.9);
transition: opacity 0.2s ease, transform 0.2s ease;
overflow: hidden;
&.show {
opacity: 1;
transform: scale(1);
}
// Мобильное позиционирование больше не отличается - убрано
// (контекстное меню позиционируется одинаково на всех устройствах)
// Backdrop для мобильных
&::before {
content: '';
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
z-index: -1;
opacity: 0;
transition: opacity 0.2s ease;
pointer-events: none;
}
&.show::before {
opacity: 1;
}
// На десктопе backdrop не нужен
@media (hover: hover) and (pointer: fine) {
&::before {
display: none;
}
}
}
/* ─────────────────────────────────────────────────────────────
MESSAGE HIGHLIGHT
────────────────────────────────────────────────────────────── */
.message-highlighted {
background-color: color-mix(in srgb, var(--bs-primary), transparent 92%) !important;
transition: background-color 0.2s ease;
}
/* ─────────────────────────────────────────────────────────────
QUICK REACTIONS (TELEGRAM-STYLE)
────────────────────────────────────────────────────────────── */
// Floating панель с реакциями (отдельно от меню)
.quick-reactions-panel {
position: fixed;
z-index: 10000; // Выше чем меню (9999)
background: #ffffff; // Явно белый для светлой темы
border-radius: 24px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
width: 280px; // Фиксированная ширина как у меню
opacity: 0;
transform: translateY(10px) scale(0.95);
transition: opacity 0.2s ease, transform 0.2s ease, height 0.3s ease;
pointer-events: none; // Не блокирует клики когда невидима
overflow: hidden; // Для плавной анимации высоты
&.show {
opacity: 1;
transform: translateY(0) scale(1);
pointer-events: auto;
}
}
.quick-reactions-wrapper {
position: relative;
display: flex;
align-items: center;
justify-content: center;
padding: 6px 8px;
}
.quick-reactions-list {
display: flex;
align-items: center;
gap: 2px;
flex-wrap: wrap; // Переносим смайлики на новые строки
overflow-x: auto;
overflow-y: hidden;
max-width: 100%;
padding: 4px 0;
// Скрываем scrollbar
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
.quick-reaction-btn {
flex-shrink: 0;
width: 36px;
height: 36px;
border: none;
background: transparent;
border-radius: 50%;
font-size: 24px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s cubic-bezier(0.34, 1.56, 0.64, 1);
position: relative;
&:hover {
transform: scale(1.25);
background: var(--bs-secondary-bg);
}
&:active {
transform: scale(1.1);
}
// Кнопка "показать больше реакций"
// TODO: Изменить серый фон на белый для светлой темы (должен совпадать с quick-reaction-btn) и кнопка раскрытия меню со смайликами при белой теме сливается с фоном (не меняется от цвета темы)
&.quick-reaction-btn-more {
opacity: 0.6;
font-size: 20px;
i {
transition: transform 0.3s ease;
}
&:hover {
opacity: 1;
transform: scale(1.25);
i {
transform: translateY(3px);
}
}
&.expanded i {
transform: rotate(180deg);
}
}
// Анимация появления по очереди
opacity: 0;
animation: reactionItemAppear 0.2s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
@for $i from 1 through 10 {
&:nth-child(#{$i}) {
animation-delay: #{$i * 0.03}s;
}
}
}
@keyframes reactionItemAppear {
from {
opacity: 0;
transform: scale(0.5) translateY(8px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
/* ─────────────────────────────────────────────────────────────
MENU ACTIONS
────────────────────────────────────────────────────────────── */
.menu-actions {
padding: var(--space-2) 0;
}
.menu-action {
width: 100%;
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
border: none;
background: transparent;
cursor: pointer;
transition: background-color 0.15s ease;
font-size: 15px;
color: var(--bs-body-color);
text-align: left;
&:hover {
background-color: var(--bs-tertiary-bg);
}
&:active {
background-color: var(--bs-secondary-bg);
}
i {
font-size: 18px;
width: 24px;
text-align: center;
color: var(--bs-secondary-color);
}
span {
flex: 1;
font-weight: 500;
}
}
.menu-action-danger {
color: var(--bs-danger);
i {
color: var(--bs-danger);
}
&:hover {
background-color: color-mix(in srgb, var(--bs-danger), transparent 95%);
}
}
/* ─────────────────────────────────────────────────────────────
TOAST NOTIFICATION
────────────────────────────────────────────────────────────── */
.context-menu-toast {
position: fixed;
bottom: 80px;
left: 50%;
transform: translateX(-50%) translateY(20px);
background: rgba(0, 0, 0, 0.85);
color: white;
padding: var(--space-3) calc(var(--space-3) * 2);
border-radius: var(--badge-radius);
font-size: 14px;
font-weight: 500;
z-index: 10000;
opacity: 0;
transition: opacity 0.3s ease, transform 0.3s ease;
pointer-events: none;
white-space: nowrap;
&.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}
/* ─────────────────────────────────────────────────────────────
DARK THEME
────────────────────────────────────────────────────────────── */
[data-bs-theme="dark"] {
.message-context-menu {
background: #2b2d31;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
.message-highlighted {
background-color: rgba(88, 101, 242, 0.15) !important;
}
.quick-reactions-panel {
background: #2b2d31;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
}
.quick-reaction-btn {
&:hover {
background: rgba(255, 255, 255, 0.1);
}
}
.menu-action {
color: #dbdee1;
&:hover {
background-color: #35373c;
}
&:active {
background-color: #3f4147;
}
i {
color: #b5bac1;
}
}
.menu-action-danger {
color: #f23f43;
i {
color: #f23f43;
}
&:hover {
background-color: rgba(242, 63, 67, 0.1);
}
}
}
/* ─────────────────────────────────────────────────────────────
UTILITIES
────────────────────────────────────────────────────────────── */
.message-context-menu-enabled .reaction-button.add-reaction {
display: none !important;
}
@keyframes vibrate {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-2px); }
75% { transform: translateX(2px); }
}
.message-vibrating {
animation: vibrate 0.1s linear;
}

View File

@ -0,0 +1,234 @@
/**
* message-inputs.scss
* Унифицированные стили для полей ввода сообщений и комментариев
*/
$message-bg-default: transparent;
$message-bg-focused: transparent;
.message-field {
display: flex;
align-items: center;
gap: var(--space-1);
width: 100%;
padding: 0.35rem 0.4rem;
border: 1px solid var(--bs-border-color);
border-radius: 999px;
background: $message-bg-default;
transition: border-color 0.15s ease, box-shadow 0.15s ease, background-color 0.15s ease;
--message-action-size: 42px;
flex-wrap: nowrap; // Важно: запрещаем перенос элементов на новую строку
}
.message-field:focus-within {
border-color: var(--bs-primary);
box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.15);
background: $message-bg-focused;
}
.message-field--stacked {
align-items: stretch;
flex-direction: column;
border-radius: 24px;
padding: 0.75rem;
gap: var(--space-1);
}
.message-field--compact {
padding: 0.25rem 0.3rem;
border-radius: 24px;
--message-action-size: 36px;
}
.message-input,
.composer-textarea {
flex: 1 1 auto;
min-width: 0; // Важно: позволяет сжиматься ниже размера контента
border: none !important;
background: transparent !important;
resize: none;
padding: 0.5rem 0.75rem;
line-height: 1.4;
font-size: 1rem;
color: inherit;
scrollbar-width: none;
box-shadow: none !important;
outline: none !important;
// Высота управляется через textareaAutoExpand.js для rows="1"
// Для остальных textarea используется стандартное поведение
}
.message-field--compact .message-input {
font-size: 0.95rem;
// min-height удален - управляется textareaAutoExpand.js
}
.message-field--stacked .message-input {
min-height: 96px;
padding: 0.35rem 0;
}
.message-input:focus {
outline: none;
box-shadow: none;
}
.message-input::-webkit-scrollbar {
display: none;
}
// Кнопки действий в формах сообщений
// Размеры и форма определяются здесь, стили (цвета, эффекты) - в custom-bootstrap.scss
.message-icon-btn,
.btn-attachment,
.btn-send,
.message-send,
.btn-emoji {
width: var(--message-action-size, 42px) !important;
min-width: var(--message-action-size, 42px) !important;
height: var(--message-action-size, 42px) !important;
border-radius: 50% !important;
display: inline-flex !important;
align-items: center;
justify-content: center;
padding: 0 !important;
aspect-ratio: 1 / 1;
flex: 0 0 auto;
line-height: 1;
font-size: 1.05rem;
--bs-btn-padding-x: 0 !important;
--bs-btn-padding-y: 0 !important;
--bs-btn-border-radius: 50% !important;
box-sizing: border-box;
}
// Кнопка отправки - дополнительная тень
.message-send,
.btn-send {
box-shadow: 0 4px 12px rgba(var(--bs-primary-rgb), 0.2);
}
.message-attach {
position: relative;
}
.message-attach .dropdown-menu {
margin-top: 0.35rem;
border-radius: 16px;
padding: 0.5rem;
}
.message-emoji .dropdown-menu {
min-width: 260px;
padding: 0;
border-radius: 16px;
overflow: hidden;
}
.chat-emoji-picker {
width: min(340px, 90vw);
max-width: 360px;
height: 340px;
border: none;
outline: none;
// Основные переменные для светлой и тёмной темы
--background-color: var(--bs-body-bg);
--text-color: var(--bs-body-color);
--category-font-color: var(--bs-secondary-color);
--hover-color: var(--bs-secondary-bg);
--button-active-background: var(--bs-primary-bg-subtle, var(--bs-primary));
--button-hover-background: var(--bs-secondary-bg);
--focus-indicator: var(--bs-primary);
--border-color: color-mix(in srgb, var(--bs-border-color), transparent 25%);
--input-border-color: color-mix(in srgb, var(--bs-border-color), transparent 30%);
--input-placeholder-color: var(--bs-secondary-color);
--input-focus-border-color: var(--bs-primary);
--indicator-color: var(--bs-primary);
// RGB переменные для emoji-picker-element (для тёмной темы)
--rgb-background: var(--bs-body-bg-rgb);
--rgb-color: var(--bs-body-color-rgb);
--rgb-accent: var(--bs-primary-rgb);
--rgb-input: var(--bs-body-bg-rgb);
// Скроллбар для светлой темы по умолчанию
--scrollbar-color: rgba(0, 0, 0, 0.3);
--scrollbar-background: rgba(0, 0, 0, 0.05);
// Скрываем кнопку выбора тона кожи
button.favorites {
display: none !important;
}
// Кастомный скроллбар для внутренней панели
::part(tabpanel) {
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-color) var(--scrollbar-background);
}
::part(tabpanel)::-webkit-scrollbar {
width: 8px;
}
::part(tabpanel)::-webkit-scrollbar-track {
background: var(--scrollbar-background);
}
::part(tabpanel)::-webkit-scrollbar-thumb {
background: var(--scrollbar-color);
border-radius: 4px;
}
::part(tabpanel)::-webkit-scrollbar-thumb:hover {
background: rgba(var(--bs-body-color-rgb), 0.4);
}
}
// Дополнительные стили для тёмной темы
[data-bs-theme="dark"] {
.chat-emoji-picker {
// Явно устанавливаем цвета для тёмной темы
--background-color: var(--bs-body-bg);
--text-color: var(--bs-body-color);
--secondary-text-color: var(--bs-secondary-color);
--category-emoji-size: 1.125rem;
// Скроллбар для темной темы
--scrollbar-color: rgba(255, 255, 255, 0.3);
--scrollbar-background: rgba(255, 255, 255, 0.05);
}
}
.message-field--solo {
display: block;
border-radius: 24px;
padding: 0.75rem 1rem;
}
.message-field--solo .message-input {
width: 100%;
min-height: 120px;
padding: 0;
}
@media (max-width: 576px) {
.message-field {
// Уменьшаем размеры кнопок на мобильных
--message-action-size: 38px;
padding: 0.3rem 0.35rem;
gap: 0.25rem;
}
.message-field--compact {
--message-action-size: 32px;
padding: 0.2rem 0.25rem;
}
.message-input,
.composer-textarea {
// Уменьшаем отступы для экономии места
padding: 0.4rem 0.5rem;
font-size: 0.95rem;
}
}

View File

@ -0,0 +1,335 @@
/**
* _message-reactions.scss
* Реакции на сообщения (эмодзи)
*/
/* ─────────────────────────────────────────────────────────────
REACTIONS CONTAINER
────────────────────────────────────────────────────────────── */
.message-reactions {
display: flex;
flex-wrap: wrap;
gap: 6px; // Увеличен gap для лучшего дыхания
margin-top: var(--space-2);
align-items: center;
}
/* ─────────────────────────────────────────────────────────────
REACTION BUTTON
────────────────────────────────────────────────────────────── */
.reaction-button {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
border: 1px solid var(--bs-border-color);
border-radius: 12px; // Скругленные края как таблетка
background: var(--bs-body-bg); // Контрастный фон (белый/черный в зависимости от темы)
cursor: pointer;
transition: all 0.2s ease;
font-size: 14px;
line-height: 1;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); // Легкая тень для отделения от фона
&:hover {
background: var(--bs-secondary-bg);
border-color: color-mix(in srgb, var(--bs-border-color), var(--bs-body-color) 30%);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
// Активная реакция (пользователь уже отреагировал)
&.active {
background: #e7f3ff; // Непрозрачный светло-голубой вместо полупрозрачного
border-color: var(--bs-primary);
color: var(--bs-primary);
box-shadow: 0 2px 4px rgba(var(--bs-primary-rgb), 0.2);
&:hover {
background: #d0e9ff; // Чуть темнее при hover
}
.count {
color: var(--bs-primary);
font-weight: 600; // Жирнее для активных
}
}
// Эмодзи внутри кнопки
.emoji {
font-size: 16px;
line-height: 1;
}
// Счётчик реакций
.count {
font-size: 12px;
font-weight: 500;
color: var(--bs-body-color);
}
}
/* ─────────────────────────────────────────────────────────────
ADD REACTION BUTTON
────────────────────────────────────────────────────────────── */
.reaction-button.add-reaction {
opacity: 0;
transition: opacity 0.2s ease;
border-style: dashed;
&:hover {
opacity: 1;
border-style: solid;
}
}
// Показывать кнопку добавления при наведении на сообщение (только на десктопе без контекстного меню)
@media (hover: hover) and (pointer: fine) {
[data-message-id]:not(.message-context-menu-enabled):hover .reaction-button.add-reaction,
.message-reactions:hover .reaction-button.add-reaction {
opacity: 1;
}
}
// Если есть хотя бы одна реакция, всегда показывать кнопку добавления
.message-reactions:has(.reaction-button:not(.add-reaction)) .add-reaction {
opacity: 1;
}
// Полностью скрыть кнопку "+" если включено контекстное меню
.message-context-menu-enabled .reaction-button.add-reaction {
display: none;
}
/* ─────────────────────────────────────────────────────────────
EMOJI PICKER
────────────────────────────────────────────────────────────── */
.emoji-picker {
background: var(--bs-body-bg);
border: 1px solid var(--bs-border-color);
border-radius: var(--radius-md);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
padding: var(--space-2);
min-width: 200px;
}
.emoji-picker-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 4px;
}
.emoji-picker-button {
width: 40px;
height: 40px;
border: 1px solid transparent;
border-radius: var(--radius-sm);
background: transparent;
cursor: pointer;
font-size: 24px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s ease;
&:hover {
background: var(--bs-tertiary-bg);
border-color: var(--bs-border-color);
transform: scale(1.1);
}
&:active {
transform: scale(0.95);
}
}
/* ─────────────────────────────────────────────────────────────
ANIMATIONS
────────────────────────────────────────────────────────────── */
@keyframes reactionPop {
0% {
transform: scale(0);
opacity: 0;
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
opacity: 1;
}
}
.reaction-button.new {
animation: reactionPop 0.3s ease;
}
/* ─────────────────────────────────────────────────────────────
MOBILE RESPONSIVE
────────────────────────────────────────────────────────────── */
@media (max-width: 576px) {
.reaction-button {
padding: 3px 6px;
font-size: 12px;
.emoji {
font-size: 14px;
}
.count {
font-size: 11px;
}
}
.emoji-picker-grid {
grid-template-columns: repeat(6, 1fr);
}
.emoji-picker-button {
width: 36px;
height: 36px;
font-size: 20px;
}
}
/* ─────────────────────────────────────────────────────────────
DARK THEME
────────────────────────────────────────────────────────────── */
[data-bs-theme="dark"] {
.reaction-button {
background: #2b3035; // Темнее чем bubble для контраста
border-color: #495057;
color: #f8f9fa;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
&:hover {
background: #343a40;
border-color: #6c757d;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
}
&.active {
background: #0d3c61;
border-color: #0d6efd;
color: #4dabf7;
box-shadow: 0 2px 4px rgba(13, 110, 253, 0.3);
&:hover {
background: #0a4a7d;
}
.count {
color: #4dabf7;
font-weight: 600;
}
}
}
.emoji-picker {
background: #212529;
border-color: #495057;
}
.emoji-picker-button:hover {
background: #343a40;
border-color: #495057;
}
.reaction-users-popup {
background: #2b3035;
border-color: #495057;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
.reaction-users-header {
border-bottom-color: #495057;
}
.user-name {
color: #f8f9fa;
&:hover {
background: #343a40;
}
}
}
}
/* ─────────────────────────────────────────────────────────────
REACTION USERS POPUP
────────────────────────────────────────────────────────────── */
.reaction-users-popup {
position: fixed;
z-index: 9999;
background: var(--bs-body-bg);
border: 1px solid var(--bs-border-color);
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
min-width: 180px;
max-width: 280px;
opacity: 0;
transform: translateY(-4px);
transition: opacity 0.2s ease, transform 0.2s ease;
pointer-events: none;
&.show {
opacity: 1;
transform: translateY(0);
pointer-events: all;
}
}
.reaction-users-header {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
border-bottom: 1px solid var(--bs-border-color);
font-weight: 600;
.reaction-emoji {
font-size: 20px;
}
.reaction-count {
color: var(--bs-secondary);
font-size: 14px;
}
}
.reaction-users-list {
max-height: 200px;
overflow-y: auto;
padding: 4px;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--bs-border-color);
border-radius: 3px;
&:hover {
background: var(--bs-secondary);
}
}
}
.user-name {
padding: 8px 12px;
color: var(--bs-body-color);
font-size: 14px;
border-radius: 6px;
transition: background-color 0.15s ease;
&:hover {
background: var(--bs-secondary-bg);
}
&:not(:last-child) {
margin-bottom: 2px;
}
}

View File

@ -0,0 +1,292 @@
/**
* _message-selection.scss
* Стили для режима выделения сообщений
*/
/* ─────────────────────────────────────────────────────────────
SELECTION MODE
────────────────────────────────────────────────────────────── */
.selection-mode .msg {
cursor: pointer;
transition: background-color 0.2s;
position: relative;
padding-left: 40px !important;
&.justify-content-end {
padding-left: 0 !important;
padding-right: 40px !important;
}
&:hover {
background-color: rgba(0, 123, 255, 0.05);
}
}
/* ─────────────────────────────────────────────────────────────
CHECKBOX
────────────────────────────────────────────────────────────── */
.msg-select-checkbox {
position: absolute;
left: 8px;
top: 50%;
transform: translateY(-50%);
width: 24px;
height: 24px;
border: 2px solid var(--bs-border-color);
border-radius: 50%;
background: var(--bs-body-bg);
display: none;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
z-index: 10;
flex-shrink: 0;
.selection-mode & {
display: flex;
}
&:hover {
border-color: var(--bs-primary);
background: color-mix(in srgb, var(--bs-primary), transparent 90%);
}
&.selected {
background: var(--bs-primary);
border-color: var(--bs-primary);
&::after {
content: '✓';
color: white;
font-size: 14px;
font-weight: bold;
}
}
}
.msg.justify-content-end .msg-select-checkbox {
left: auto;
right: 8px;
}
.msg.selected {
background-color: color-mix(in srgb, var(--bs-primary), transparent 90%);
}
/* ─────────────────────────────────────────────────────────────
ACTIONS BAR
────────────────────────────────────────────────────────────── */
.selection-actions-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: var(--bs-body-bg);
border-top: 2px solid var(--bs-primary);
padding: 12px 20px;
display: none;
align-items: center;
justify-content: space-between;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
z-index: $z-modal;
animation: slideUp 0.3s ease-out;
&.active {
display: flex;
}
}
@keyframes slideUp {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
.selection-count {
font-weight: 600;
color: var(--bs-primary);
font-size: 16px;
}
.selection-actions {
display: flex;
gap: 10px;
.btn {
padding: 8px 20px;
font-weight: 500;
border-radius: var(--button-radius);
transition: all 0.2s;
&:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
}
}
/* ─────────────────────────────────────────────────────────────
FORWARD MODAL
────────────────────────────────────────────────────────────── */
.chat-forward-modal {
.modal-dialog {
max-width: 500px;
}
}
.chat-list-item {
padding: 12px 16px;
border-bottom: 1px solid var(--bs-border-color);
cursor: pointer;
transition: background-color 0.2s;
display: flex;
align-items: center;
gap: 12px;
&:hover {
background-color: var(--bs-tertiary-bg);
}
&:last-child {
border-bottom: none;
}
}
.chat-list-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
background: var(--bs-tertiary-bg);
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
color: var(--bs-secondary-color);
flex-shrink: 0;
overflow: hidden;
position: relative;
img {
width: 100%;
height: 100%;
object-fit: cover;
position: absolute;
top: 0;
left: 0;
}
}
.chat-list-info {
flex: 1;
min-width: 0;
}
.chat-list-name {
font-weight: 600;
color: var(--bs-body-color);
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.chat-list-subtitle {
font-size: 13px;
color: var(--bs-secondary-color);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.chat-list-loading,
.chat-list-empty {
padding: 40px 20px;
text-align: center;
color: var(--bs-secondary-color);
}
/* ─────────────────────────────────────────────────────────────
SEARCH
────────────────────────────────────────────────────────────── */
.chat-forward-search {
padding: 12px 16px;
border-bottom: 1px solid var(--bs-border-color);
input {
width: 100%;
padding: 8px 12px;
border: 1px solid var(--bs-border-color);
border-radius: var(--radius-md);
font-size: 14px;
background: var(--bs-body-bg);
color: var(--bs-body-color);
&:focus {
outline: none;
border-color: var(--bs-primary);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--bs-primary), transparent 90%);
}
&::placeholder {
color: var(--bs-secondary-color);
}
}
}
/* ─────────────────────────────────────────────────────────────
FORWARDED INDICATOR
────────────────────────────────────────────────────────────── */
.forwarded-indicator {
font-size: 12px;
color: var(--bs-secondary-color);
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 4px;
font-style: italic;
i {
font-size: 14px;
}
}
/* ─────────────────────────────────────────────────────────────
MOBILE RESPONSIVE
────────────────────────────────────────────────────────────── */
@media (max-width: 768px) {
.selection-actions-bar {
padding: 10px 15px;
}
.selection-count {
font-size: 14px;
}
.selection-actions .btn {
padding: 6px 16px;
font-size: 14px;
}
.msg-select-checkbox {
width: 20px;
height: 20px;
left: 5px;
}
.msg.justify-content-end .msg-select-checkbox {
right: 5px;
}
.selection-mode .msg {
padding-left: 32px !important;
&.justify-content-end {
padding-left: 0 !important;
padding-right: 32px !important;
}
}
}

View File

@ -0,0 +1,201 @@
// =============================================================================
// Custom Bootstrap Configuration
// =============================================================================
// Этот файл переопределяет переменные Bootstrap ДО его компиляции
// Результат: полностью кастомизированный Bootstrap с нашей системой отступов
// =============================================================================
// 1. ИМПОРТ НАШИХ ПЕРЕМЕННЫХ
// =============================================================================
// Сначала импортируем наши CSS переменные (для справки)
// Реальные значения ниже в SCSS переменных
// =============================================================================
// 2. ПЕРЕОПРЕДЕЛЕНИЕ BOOTSTRAP ПЕРЕМЕННЫХ
// =============================================================================
// ──────────────────────────────────────────────────────────────
// 2.1 Система отступов (8px grid)
// ──────────────────────────────────────────────────────────────
// Bootstrap использует $spacer для всех utility классов (.p-*, .m-* и т.д.)
// Переопределяем его на нашу 8px сетку
$spacer: 1rem !default; // 16px - базовое значение
// Переопределение всей шкалы spacing
$spacers: (
0: 0, // 0px
1: 0.25rem, // 4px - var(--space-1)
2: 0.5rem, // 8px - var(--space-2)
3: 0.75rem, // 12px - var(--space-3)
4: 1rem, // 16px - var(--space-4)
5: 1.5rem, // 24px - var(--space-5)
) !default;
// ──────────────────────────────────────────────────────────────
// 2.2 Grid система
// ──────────────────────────────────────────────────────────────
$grid-gutter-width: 0.5rem !default; // 8px - var(--space-2)
// ──────────────────────────────────────────────────────────────
// 2.3 Компоненты - Padding
// ──────────────────────────────────────────────────────────────
// Переопределяем padding для всех Bootstrap компонентов
// Cards
$card-spacer-y: 1rem !default; // var(--space-2)
$card-spacer-x: 1rem !default; // var(--space-2)
// Alerts
$alert-padding-y: 1rem !default; // var(--space-2)
$alert-padding-x: 1rem !default; // var(--space-2)
// Buttons
$btn-padding-y: 0.5rem !default; // var(--space-1)
$btn-padding-x: 1rem !default; // var(--space-2)
$btn-padding-y-sm: 0.5rem !default; // var(--space-1)
$btn-padding-x-sm: 0.5rem !default; // var(--space-1)
$btn-padding-y-lg: 1rem !default; // var(--space-2)
$btn-padding-x-lg: 1.5rem !default; // var(--space-3)
// Form controls
$input-padding-y: 0.5rem !default; // var(--space-1)
$input-padding-x: 1rem !default; // var(--space-2)
$input-padding-y-sm: 0.5rem !default; // var(--space-1)
$input-padding-x-sm: 0.5rem !default; // var(--space-1)
$input-padding-y-lg: 1rem !default; // var(--space-2)
$input-padding-x-lg: 1.5rem !default; // var(--space-3)
// Modals
$modal-inner-padding: 1rem !default; // var(--space-2)
$modal-header-padding-y: 1rem !default; // var(--space-2)
$modal-header-padding-x: 1rem !default; // var(--space-2)
// Dropdowns
$dropdown-padding-x: 0 !default;
$dropdown-padding-y: 8px !default; // var(--space-1)
$dropdown-item-padding-y: 8px !default; // var(--space-1)
$dropdown-item-padding-x: 16px !default; // var(--space-2)
// Navs
$nav-link-padding-y: 8px !default; // var(--space-1)
$nav-link-padding-x: 16px !default; // var(--space-2)
// Navbar
$navbar-padding-y: 8px !default; // var(--space-1)
$navbar-padding-x: 16px !default; // var(--space-2)
// Pagination
$pagination-padding-y: 8px !default; // var(--space-1)
$pagination-padding-x: 16px !default; // var(--space-2)
$pagination-padding-y-sm: 8px !default; // var(--space-1)
$pagination-padding-x-sm: 8px !default; // var(--space-1)
$pagination-padding-y-lg: 16px !default; // var(--space-2)
$pagination-padding-x-lg: 24px !default; // var(--space-3)
// List groups
$list-group-item-padding-y: 1rem !default; // var(--space-2)
$list-group-item-padding-x: 1rem !default; // var(--space-2)
// Breadcrumbs
$breadcrumb-padding-y: 0.5rem !default; // var(--space-1)
$breadcrumb-padding-x: 1rem !default; // var(--space-2)
// Toasts
$toast-padding-x: 1rem !default; // var(--space-2)
$toast-padding-y: 0.5rem !default; // var(--space-1)
// Offcanvas
$offcanvas-padding-y: 1rem !default; // var(--space-2)
$offcanvas-padding-x: 1rem !default; // var(--space-2)
// Badges
$badge-padding-y: 0.5rem !default; // var(--space-1)
$badge-padding-x: 0.5rem !default; // var(--space-1)
// ──────────────────────────────────────────────────────────────
// 2.4 Cards - переопределяем стили карточек (GLASSMORPHISM)
// ──────────────────────────────────────────────────────────────
$card-border-radius: 18px !default;
$card-border-width: 1px !default;
$card-border-color: rgba(255, 255, 255, 0.1) !default; // Светлая граница для glassmorphism
$card-bg: rgba(255, 255, 255, 0.05) !default; // Полупрозрачный для эффекта стекла
$card-box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37) !default; // Глубокая тень
// Card header/footer
$card-cap-padding-y: 1rem !default;
$card-cap-padding-x: 1rem !default;
$card-cap-bg: transparent !default; // Прозрачный header/footer
$card-cap-color: null !default;
// ──────────────────────────────────────────────────────────────
// 2.5 Дополнительные настройки (опционально)
// ──────────────────────────────────────────────────────────────
// Border radius (если хотите унифицировать)
// $border-radius: 10px !default; // var(--radius-md)
// $border-radius-sm: 6px !default; // var(--radius-sm)
// $border-radius-lg: 14px !default; // var(--radius-lg)
// =============================================================================
// 3. ИМПОРТ ABSTRACTS (Design Tokens & Mixins)
// =============================================================================
// Импортируем переменные и миксины ДО Bootstrap
@import "abstracts/variables";
@import "abstracts/mixins";
// =============================================================================
// 4. ИМПОРТ BOOTSTRAP
// =============================================================================
// Импортируем Bootstrap ПОСЛЕ переопределения переменных
@import "../node_modules/bootstrap/scss/bootstrap";
// =============================================================================
// 4.5 CSS ПЕРЕМЕННЫЕ (RUNTIME)
// =============================================================================
// CSS custom properties для использования в браузере
@import "abstracts/css-variables";
// =============================================================================
// 5. КАСТОМНЫЕ КОМПОНЕНТЫ
// =============================================================================
// Layout
@import "layout/chat-layout";
// Базовые компоненты
@import "components/common";
@import "components/navbar";
@import "components/sidebar";
@import "components/page-header";
@import "components/cards";
// Badge компоненты
@import "components/badges-bem";
// Чат компоненты (в подпапке chat/)
@import "components/chat/message-inputs";
@import "components/chat/chat-detail";
@import "components/chat/chat-date-groups";
@import "components/chat/chat-list-enhanced";
@import "components/chat/chat-enhanced";
@import "components/chat/chat-polls";
@import "components/chat/chat-new-messages-indicator";
@import "components/chat/message-selection";
@import "components/chat/message-context-menu";
@import "components/chat/message-reactions";
// Дополнительные компоненты
@import "components/comments";
@import "components/modals";
// Остальные компоненты (загружаются напрямую из CSS - не мигрировано)
// TODO: Мигрировать buttons, card-list, document-list, request-list,
// ios-search, feed-specific, base-app, request-detail

View File

@ -0,0 +1,397 @@
// =============================================================================
// Base App Layout & Styles (SCSS)
// =============================================================================
// Базовые стили приложения: layout, navbar, sidebar, формы, feed-элементы
//
// Зависимости:
// - Bootstrap custom build
// - CSS переменные из abstracts/_css-variables.scss
// =============================================================================
// ПЕРЕМЕННЫЕ
// =============================================================================
// Цвета
$primary-light: #007AFF;
$primary-dark: #0A84FF;
$link-hover-light: #0062CC;
$link-hover-dark: #59AFFF;
// Фоны темной темы
$dark-bg: #1C1C1E;
$dark-color: #F2F2F7;
$dark-border: #3A3A3C;
$dark-tertiary-bg: #2C2C2E;
// Типографика
$font-family-base: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
$font-size-base: 17px;
// Размеры компонентов
$btn-border-radius: 10px;
$btn-min-height: 44px;
$form-control-border-radius: 10px;
$form-control-height: 44px;
$dropdown-border-radius: 13px;
// Переходы
$btn-transition: background-color 0.3s, transform 0.1s;
$form-transition: box-shadow 0.2s ease, border-color 0.2s ease;
$ghost-btn-transition: background 0.2s ease;
// Тени
$dropdown-shadow: 0 6px 24px rgba(0, 0, 0, 0.12);
// Navbar
$navbar-border-width: 0.5px;
// =============================================================================
// ЦВЕТОВАЯ СХЕМА
// =============================================================================
:root {
--bs-primary: #{$primary-light};
--bs-link-color: var(--bs-primary);
--bs-link-hover-color: #{$link-hover-light};
}
[data-bs-theme="dark"] {
--bs-body-bg: #{$dark-bg};
--bs-body-color: #{$dark-color};
--bs-border-color: #{$dark-border};
--bs-tertiary-bg: #{$dark-tertiary-bg};
--bs-primary: #{$primary-dark};
--bs-link-color: var(--bs-primary);
--bs-link-hover-color: #{$link-hover-dark};
}
// =============================================================================
// BODY & БАЗОВЫЕ СТИЛИ
// =============================================================================
body {
font-family: $font-family-base;
font-size: $font-size-base;
padding-top: var(--navbar-h);
overflow-x: hidden;
background-color: var(--bs-body-bg);
// Safe area для iOS
@supports(padding-top: env(safe-area-inset-top)) {
padding-top: calc(var(--navbar-h) + env(safe-area-inset-top));
}
}
// =============================================================================
// ФОРМЫ И КНОПКИ (БАЗОВЫЕ ПЕРЕОПРЕДЕЛЕНИЯ)
// =============================================================================
.btn {
border-radius: $btn-border-radius;
min-height: $btn-min-height;
transition: $btn-transition;
&:hover {
transform: scale(1.02);
}
}
.form-control {
border-radius: $form-control-border-radius;
height: $form-control-height;
font-size: $font-size-base;
&:focus {
box-shadow: 0 0 0 2px rgba(var(--bs-primary-rgb), 0.25);
border-color: var(--bs-primary);
}
}
.dropdown-menu {
border-radius: $dropdown-border-radius;
box-shadow: $dropdown-shadow;
}
// =============================================================================
// NAVBAR
// =============================================================================
.app-navbar {
position: fixed;
inset: 0 0 auto 0;
z-index: 1030;
border-bottom: $navbar-border-width solid var(--bs-border-color);
}
// =============================================================================
// LAYOUT GRID SYSTEM
// =============================================================================
.app-layout {
display: block;
}
// Адаптивный контейнер
.content-container {
width: 100%;
margin-left: auto;
margin-right: auto;
}
// Мобильные устройства
@media (max-width: 991.98px) {
.content-container {
padding-left: var(--bs-gutter-x, 0.75rem);
padding-right: var(--bs-gutter-x, 0.75rem);
}
}
// Десктоп с grid layout
@media (min-width: 992px) {
// При наличии сайдбаров убираем padding и max-width
.app-layout.with-sidebar,
.app-layout.with-rightbar {
.content-container {
max-width: none;
padding-left: 0;
padding-right: 0;
}
}
// Grid: sidebar + content + rightbar
.app-layout.with-sidebar.with-rightbar {
display: grid;
grid-template-columns: var(--sidebar-w) 1fr var(--rightbar-w);
gap: var(--layout-gap);
align-items: start;
}
// Grid: sidebar + content
.app-layout.with-sidebar:not(.with-rightbar) {
display: grid;
grid-template-columns: var(--sidebar-w) 1fr;
gap: var(--layout-gap);
align-items: start;
}
// Без сайдбаров - стандартный container
.app-layout:not(.with-sidebar):not(.with-rightbar) {
.content-container {
padding-left: var(--bs-gutter-x, 0.75rem);
padding-right: var(--bs-gutter-x, 0.75rem);
max-width: 960px;
}
}
// Компенсация для .row внутри grid-layout
.app-layout.with-sidebar,
.app-layout.with-rightbar {
.content-container > .row {
margin-left: 0;
margin-right: 0;
> [class*="col-"] {
padding-left: calc(var(--bs-gutter-x) * 0.5);
padding-right: calc(var(--bs-gutter-x) * 0.5);
}
}
// Карточки внутри колонок
.container > .row > [class*="col-"] {
> .card,
> section.card {
max-width: 100%;
min-width: 0;
overflow: hidden;
}
}
}
}
// Breakpoints для max-width
@media (min-width: 1200px) {
.app-layout:not(.with-sidebar):not(.with-rightbar) .content-container {
max-width: 1140px;
}
}
@media (min-width: 1400px) {
.app-layout:not(.with-sidebar):not(.with-rightbar) .content-container {
max-width: 1320px;
}
}
// =============================================================================
// SIDEBAR
// =============================================================================
#sidebar {
--bs-offcanvas-width: var(--sidebar-w);
border-radius: 0 13px 13px 0;
@media (min-width: 992px) {
&.offcanvas-lg {
position: sticky;
top: var(--navbar-h);
height: calc(100dvh - var(--navbar-h));
align-self: start;
overflow: auto;
border-right: $navbar-border-width solid var(--bs-border-color);
background: var(--bs-body-bg);
transform: none !important;
visibility: visible !important;
}
}
}
// =============================================================================
// NOTIFICATION BELL
// =============================================================================
.bell-wrapper {
position: relative;
line-height: 1;
display: inline-block;
}
.bell-dot {
position: absolute;
top: -2px;
right: -2px;
width: 9px;
height: 9px;
border-radius: 999px;
background: var(--bs-danger);
color: #fff;
font-size: 7px;
line-height: 9px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.65);
}
// =============================================================================
// BUTTONS & GHOST BUTTONS
// =============================================================================
.btn-ghost {
background: color-mix(in srgb, var(--bs-body-bg) 94%, transparent);
color: var(--bs-body-color);
padding: var(--space-1);
border-radius: 999px;
font-size: 0.9rem;
transition: $ghost-btn-transition;
&:hover {
background: color-mix(in srgb, var(--bs-body-bg) 88%, transparent);
}
.bi {
font-size: 1rem;
margin-right: 0.35rem;
vertical-align: -2px;
}
// Текст кнопок с toggle (Скрыть/Показать)
.txt-close {
display: none;
}
&[aria-expanded="true"] {
.txt-open {
display: none;
}
.txt-close {
display: inline;
}
}
}
// =============================================================================
// FEED ELEMENTS
// =============================================================================
// Блоки типа "Новые сотрудники"
.feed-new {
border-radius: var(--feed-radius);
border: 1px solid color-mix(in srgb, var(--bs-border-color), transparent 10%);
background: var(--bs-body-bg);
box-shadow: var(--feed-shadow);
padding: var(--card-padding);
}
// Z-index для stretched-link
.card-body .stretched-link::after,
.feed-img .stretched-link::after {
z-index: 1;
}
.collapse {
position: relative;
z-index: 2;
}
// =============================================================================
// ALPHA HEADERS (буквенные разделители)
// =============================================================================
.alpha-header {
position: sticky;
top: calc(var(--navbar-h) + var(--space-1));
z-index: 1;
background: color-mix(in srgb, var(--bs-body-bg) 92%, transparent);
backdrop-filter: blur(6px);
border: 1px solid color-mix(in srgb, var(--bs-border-color), transparent 30%);
color: var(--bs-secondary-color);
display: inline-flex;
align-items: center;
gap: var(--space-1);
padding: var(--space-1);
border-radius: 999px;
font-weight: 700;
font-size: 0.85rem;
margin: var(--space-1) 0 var(--space-1) var(--space-1);
}
.alpha-dot {
width: 6px;
height: 6px;
border-radius: 999px;
background: var(--bs-primary);
opacity: 0.6;
}
// =============================================================================
// ACTION BUTTONS
// =============================================================================
.action-btn {
min-height: unset;
padding: var(--space-1);
.bi {
font-size: 1rem;
}
}
// =============================================================================
// TEXT UTILITIES
// =============================================================================
// Переносим длинные слова/ссылки
.feed-text,
.feed-full {
overflow-wrap: anywhere;
width: 100%;
margin-right: 0;
padding-right: 0;
}
// Z-index для кнопки "Подробнее"
.readmore-toggle {
position: relative;
z-index: 2;
}

View File

@ -0,0 +1,61 @@
// =============================================================================
// Chat Layout
// =============================================================================
// Глобальная структура страниц чата (не специфичные компоненты)
// =============================================================================
// 1. MAIN CONTAINER
// =============================================================================
main.main {
padding-bottom: 0 !important;
}
// Специальное правило для страниц чата в grid layout
// Ограничиваем высоту main, чтобы не ломать grid и rightbar
@media (min-width: 992px) {
.app-layout.with-rightbar main.main:has(.chat-page),
.app-layout.with-sidebar main.main:has(.chat-page) {
max-height: calc(100vh - var(--navbar-h, 60px) - 2 * var(--layout-padding-y, 12px));
overflow: hidden;
}
}
// =============================================================================
// 2. CHAT PAGE STRUCTURE
// =============================================================================
// Root контейнер страницы чата
@supports (height: 100svh) {
.chat-root {
height: calc(100svh - var(--navbar-h));
}
}
.chat-root > .row {
height: 100%;
min-height: 0;
}
// Column layout
.chat-col {
display: flex;
flex-direction: column;
height: 93%;
min-height: 0;
}
// =============================================================================
// 3. CHAT BOX CONTAINER
// =============================================================================
.card.chat-box {
position: relative;
flex: 1 1 auto;
min-height: 0;
display: grid;
grid-template-rows: 1fr auto; /* messages + composer */
overflow: hidden;
margin-bottom: 0;
background: var(--bs-body-bg);
}

View File

@ -0,0 +1,85 @@
// =============================================================================
// Main Application SCSS
// =============================================================================
// Главный файл для сборки всех стилей приложения (кроме Bootstrap)
// Компилируется в css/app.css
// =============================================================================
// 1. ABSTRACTS - Переменные, миксины, функции
// =============================================================================
@import 'abstracts/index';
// =============================================================================
// 2. LAYOUT - Структура приложения
// =============================================================================
@import 'layout/base-app';
@import 'layout/chat-layout';
// =============================================================================
// 3. COMPONENTS - Переиспользуемые компоненты
// =============================================================================
// Базовые компоненты
@import 'components/buttons';
@import 'components/cards';
@import 'components/modals';
@import 'components/navbar';
@import 'components/sidebar';
@import 'components/page-header';
@import 'components/common';
// Spacing и layout utilities
@import 'components/layout-spacing';
@import 'components/spacing-utils';
// iOS-стиль компоненты
@import 'components/ios-search';
@import 'components/ios-components';
// Бейджи
@import 'components/badges-bem';
@import 'components/badges-telegram-style';
// Комментарии
@import 'components/comments';
// Списки и уведомления
@import 'components/card-list';
@import 'components/notifications';
// Заявления и документы
@import 'components/request-list';
@import 'components/request-detail';
@import 'components/document-list';
@import 'components/recipientPicker';
@import 'components/list-view';
// Feed компоненты
@import 'components/feed-specific';
@import 'components/join-scroller';
@import 'components/post-form';
// Модальные окна
@import 'components/modal-overrides';
@import 'components/logout-modal';
// Employees компоненты
@import 'components/department-controls';
@import 'components/team-wheel';
// Календарь и rightbar
@import 'components/rightbar-calendar';
// =============================================================================
// 4. UTILITIES - Вспомогательные классы
// =============================================================================
// @import 'utilities/spacing-utils';
// @import 'utilities/index';
// =============================================================================
// ПРИМЕЧАНИЯ
// =============================================================================
// - Bootstrap подключается отдельно через bootstrap-custom.css
// - CSS переменные (--variable) определены в abstracts/_css-variables.scss
// - SCSS переменные ($variable) определены в abstracts/_variables.scss
// - Порядок импортов критичен для CSS каскада

View File

@ -0,0 +1,461 @@
# Аудит архитектуры backend/static/scss
**Дата:** 15 января 2026
**Аудитор:** GitHub Copilot
**Статус:** ⚠️ Требуется улучшение
## 📊 Общая оценка: 7/10
### ✅ Сильные стороны (7 баллов)
1. **Хорошая структура папок** (9/10)
- ✅ Логичное разделение: abstracts, layout, components, utilities
- ✅ Подпапка chat для chat компонентов
- ✅ Понятная организация
2. **Именование файлов** (8/10)
- ✅ Префикс `_` для partials
- ✅ Kebab-case naming
- ✅ Понятные имена
3. **Документация** (7/10)
- ✅ Есть BUTTONS_README.md
- ✅ Комментарии в main.scss
- ⚠️ Нет общего README в scss/
4. **Модульность** (9/10)
- ✅ 30 компонентов в components/
- ✅ 10 chat модулей в components/chat/
- ✅ Разделение abstracts
## ⚠️ Проблемы и недостатки (3 балла снижения)
### 🔴 Критические проблемы
#### 1. Дублирование импортов компонентов (CRITICAL)
**Проблема:** Многие компоненты импортируются дважды:
- Один раз в `main.scss` → компилируется в `app.css`
- Второй раз в `custom-bootstrap.scss` → компилируется в `bootstrap-custom.css`
**Дублированные импорты:**
```scss
// В main.scss:
@import 'components/common';
@import 'components/navbar';
@import 'components/sidebar';
@import 'components/page-header';
@import 'components/cards';
@import 'components/modals';
@import 'components/comments';
// И ЕЩЕ РАЗ в custom-bootstrap.scss:
@import "components/common";
@import "components/navbar";
@import "components/sidebar";
@import "components/page-header";
@import "components/cards";
@import "components/modals";
@import "components/comments";
```
**Последствия:**
- ❌ Эти стили дублируются в `app.css` И `bootstrap-custom.css`
- ❌ Увеличенный размер файлов
- ❌ Повторяющиеся селекторы
- ❌ Потенциальные конфликты специфичности
**Затронутые компоненты:**
1. common
2. navbar
3. sidebar
4. page-header
4. cards
5. badges-bem
6. modals
7. comments
**Размер дублирования:** ~1,500-2,000 строк SCSS дублируются!
#### 2. Chat компоненты ТОЛЬКО в bootstrap-custom.css
**Проблема:** Все 10 chat компонентов импортируются ТОЛЬКО в `custom-bootstrap.scss`:
```scss
// Чат компоненты (в подпапке chat/)
@import "components/chat/message-inputs";
@import "components/chat/chat-detail";
@import "components/chat/chat-date-groups";
@import "components/chat/chat-list-enhanced";
@import "components/chat/chat-enhanced";
@import "components/chat/chat-polls";
@import "components/chat/chat-new-messages-indicator";
@import "components/chat/message-selection";
@import "components/chat/message-context-menu";
@import "components/chat/message-reactions";
```
**Последствия:**
- ❌ Chat стили попадают в `bootstrap-custom.css` (301 KB)
- ❌ Нелогично - chat не относится к Bootstrap
- ❌ Сложнее поддерживать - непонятно где искать
#### 3. Устаревший TODO комментарий
В `custom-bootstrap.scss` строки 197-199:
```scss
// Остальные компоненты (загружаются напрямую из CSS - не мигрировано)
// TODO: Мигрировать buttons, card-list, document-list, request-list,
// ios-search, feed-specific, base-app, request-detail
```
**Проблема:** Эти компоненты УЖЕ мигрированы и импортируются в `main.scss`!
### 🟡 Средние проблемы
#### 4. Пустая папка utilities/
```
scss/utilities/ - ПУСТО
```
В `main.scss` закомментированы импорты:
```scss
// @import 'utilities/spacing-utils';
// @import 'utilities/index';
```
**Проблема:**
- Папка существует но не используется
- spacing-utils лежит в components/, а не utilities/
- Запутанная структура
#### 5. Нет главного README.md в scss/
**Отсутствует:** `backend/static/scss/README.md`
Есть только:
- `components/BUTTONS_README.md` - документация одного компонента
- Нет общего описания архитектуры
#### 6. Layout компоненты в custom-bootstrap.scss
```scss
// Layout
@import "layout/chat-layout";
```
И в main.scss:
```scss
@import 'layout/base-app';
@import 'layout/chat-layout'; // ДУБЛЬ!
```
**chat-layout импортируется дважды!**
### 🟢 Мелкие замечания
#### 7. Несогласованность комментариев
В `main.scss`:
```scss
// 4. UTILITIES - Вспомогательные классы
// @import 'utilities/spacing-utils';
// @import 'utilities/index';
```
Но `_spacing-utils.scss` находится в `components/`, не в `utilities/`!
#### 8. Размер custom-bootstrap.scss
202 строки, из них:
- ~70 строк - переопределения переменных Bootstrap
- ~30 строк - импорты компонентов (многие дублируются)
- ~100 строк - комментарии
Файл становится перегруженным.
## 📈 Статистика
### Размеры
- **Всего SCSS файлов:** 51
- **Всего строк SCSS:** 11,394
- **Компонентов в components/:** 30
- **Компонентов в components/chat/:** 10
- **Layout файлов:** 2
- **Abstracts файлов:** 4
### Импорты
- **Импортов в main.scss:** 30
- **Импортов в custom-bootstrap.scss:** 25
- **Дублированных импортов:** 8 компонентов
### Compiled размеры
- **app.css:** 95 KB
- **bootstrap-custom.css:** 301 KB
- **Дубликаты в обоих:** ~15-20 KB
## 🎯 Рекомендации по улучшению
### Приоритет 1: Устранить дублирование (CRITICAL)
#### Решение A: Разделить компоненты по назначению
**app.css** (main.scss):
- Все application-specific компоненты
- Layout (base-app, chat-layout)
- Components (buttons, cards, modals, etc.)
- Chat components
- Spacing utilities
**bootstrap-custom.css** (custom-bootstrap.scss):
- ТОЛЬКО переопределения Bootstrap переменных
- ТОЛЬКО импорт Bootstrap
- CSS variables (abstracts/css-variables)
- Убрать ВСЕ импорты компонентов
#### Решение B: Создать отдельный chat.css
**chat.css** (новый entry point):
- Все chat компоненты
- Chat layout
- Подключать только на страницах чата
### Приоритет 2: Очистить custom-bootstrap.scss
**Удалить из custom-bootstrap.scss:**
```scss
// ❌ УДАЛИТЬ ВСЕ ЭТО:
@import "layout/chat-layout";
@import "components/common";
@import "components/navbar";
@import "components/sidebar";
@import "components/page-header";
@import "components/cards";
@import "components/badges-bem";
@import "components/chat/..."; // все 10 импортов
@import "components/comments";
@import "components/modals";
```
**Оставить только:**
```scss
// 1. Переопределение Bootstrap переменных
$spacer: 1rem;
$spacers: (...);
// и т.д.
// 2. Импорт наших abstracts
@import "abstracts/variables";
@import "abstracts/mixins";
// 3. Импорт Bootstrap
@import "../node_modules/bootstrap/scss/bootstrap";
// 4. CSS переменные
@import "abstracts/css-variables";
// ВСЕ! Никаких компонентов!
```
### Приоритет 3: Переместить spacing-utils
```bash
# Переместить из components/ в utilities/
mv scss/components/_spacing-utils.scss scss/utilities/
mv scss/components/_layout-spacing.scss scss/utilities/
```
Раскомментировать в main.scss:
```scss
// 4. UTILITIES - Вспомогательные классы
@import 'utilities/layout-spacing';
@import 'utilities/spacing-utils';
```
### Приоритет 4: Документация
Создать `scss/README.md`:
```markdown
# SCSS Architecture
## Structure
- abstracts/ - Variables, mixins, functions
- layout/ - Page layouts
- components/ - Reusable components
- utilities/ - Utility classes
## Build
- main.scss → app.css (application styles)
- bootstrap-custom.scss → bootstrap-custom.css (Bootstrap only)
## Components
[список всех компонентов]
```
### Приоритет 5: Удалить устаревшие TODO
Удалить из custom-bootstrap.scss:
```scss
// Остальные компоненты (загружаются напрямую из CSS - не мигрировано)
// TODO: Мигрировать buttons, card-list, document-list, request-list,
// ios-search, feed-specific, base-app, request-detail
```
Все эти компоненты УЖЕ в main.scss!
## 📋 План действий (по шагам)
### Шаг 1: Backup текущего состояния
```bash
cp scss/custom-bootstrap.scss scss/custom-bootstrap.scss.backup
cp scss/main.scss scss/main.scss.backup
```
### Шаг 2: Очистить custom-bootstrap.scss
Удалить все импорты компонентов, оставить только:
- Переопределения переменных Bootstrap
- Импорт abstracts
- Импорт Bootstrap
- Импорт css-variables
### Шаг 3: Переместить chat в main.scss
Добавить в main.scss все chat компоненты:
```scss
// Chat компоненты
@import 'components/chat/message-inputs';
@import 'components/chat/chat-detail';
// ... все 10 компонентов
```
### Шаг 4: Переместить spacing в utilities
```bash
mv scss/components/_spacing-utils.scss scss/utilities/
mv scss/components/_layout-spacing.scss scss/utilities/
```
### Шаг 5: Обновить импорты в main.scss
```scss
// Utilities
@import 'utilities/layout-spacing';
@import 'utilities/spacing-utils';
```
### Шаг 6: Пересобрать CSS
```bash
npm run build:app
npm run build:bootstrap
```
### Шаг 7: Проверить размеры
```bash
ls -lh css/app.css css/bootstrap-custom.css
```
**Ожидаемый результат:**
- app.css: увеличится на ~15-20 KB (добавятся chat компоненты)
- bootstrap-custom.css: уменьшится на ~15-20 KB (удалятся дубли)
### Шаг 8: Создать документацию
Создать `scss/README.md` с описанием архитектуры
### Шаг 9: Тестирование
1. Проверить главную страницу
2. Проверить страницы с чатом
3. Проверить все компоненты
## 📊 Ожидаемые улучшения
После рефакторинга:
### До:
```
app.css: 95 KB (без chat, с дублями)
bootstrap-custom.css: 301 KB (с chat, с дублями)
Дублирование: ~15-20 KB
```
### После:
```
app.css: ~110 KB (с chat, без дублей)
bootstrap-custom.css: ~280 KB (только Bootstrap, без дублей)
Дублирование: 0 KB
Экономия: ~6-7% от общего размера
```
### Преимущества:
- ✅ Нет дубликатов кода
- ✅ Чистое разделение ответственности
- ✅ Логичная структура
- ✅ Легче поддерживать
- ✅ Меньший итоговый размер
## 🎓 Best Practices нарушения
### ❌ Текущие нарушения:
1. **DRY (Don't Repeat Yourself)** - дублирование 8 компонентов
2. **Separation of Concerns** - chat компоненты в bootstrap файле
3. **Single Responsibility** - custom-bootstrap делает слишком много
4. **Clear Architecture** - utilities/ пуста, spacing в components/
### ✅ После рефакторинга:
1. **DRY** - каждый компонент импортируется один раз
2. **Separation of Concerns** - четкое разделение: Bootstrap отдельно, app отдельно
3. **Single Responsibility** - каждый файл имеет одну четкую цель
4. **Clear Architecture** - логичная структура папок
## 💡 Дополнительные рекомендации
### 1. Рассмотреть переход на @use/@forward
SCSS @import deprecated, современный подход:
```scss
@use 'abstracts/variables' as vars;
@use 'components/buttons';
```
### 2. Создать index.scss в папках
```scss
// components/chat/_index.scss
@forward 'message-inputs';
@forward 'chat-detail';
// ...
// Использование:
@import 'components/chat'; // импортирует все
```
### 3. Добавить linter
```bash
npm install -D stylelint stylelint-config-sass-guidelines
```
### 4. Создать style guide
Документировать:
- Naming conventions
- File organization
- Import order
- Variables usage
## 🏁 Заключение
**Текущая архитектура:** 7/10 - Хорошая база, но есть критические проблемы
**Основные проблемы:**
1. Дублирование импортов (CRITICAL)
2. Chat компоненты в bootstrap-custom (HIGH)
3. Неиспользуемая utilities/ папка (MEDIUM)
**Действия:**
1. Срочно устранить дублирование
2. Переорганизовать структуру
3. Создать документацию
**После рефакторинга ожидается:** 9/10 - Чистая, поддерживаемая архитектура
---
**Дата:** 15 января 2026
**Статус:** Требуется рефакторинг
**Приоритет:** HIGH

File diff suppressed because it is too large Load Diff