diff --git a/src/components/ChatPanel.ts b/src/components/ChatPanel.ts index dd5ec07..fd8a4e7 100644 --- a/src/components/ChatPanel.ts +++ b/src/components/ChatPanel.ts @@ -63,6 +63,12 @@ export class ChatPanel { private currentReferences: FileReference[] = []; private isProcessingReference = false; + // Свойства для обсерверов прокрутки + private resizeObserver: ResizeObserver | null = null; + private scrollDebounceTimer: number | null = null; // Таймер для debounce + private cachedScrollContainer: HTMLElement | null = null; + private cachedBtnWrapper: HTMLElement | null = null; + constructor(container: HTMLElement, props: ChatPanelProps) { this.container = container; this.props = props; @@ -429,6 +435,11 @@ export class ChatPanel { } destroy(): void { + if (this.scrollDebounceTimer !== null) { + window.clearTimeout(this.scrollDebounceTimer); + } + if (this.resizeObserver) this.resizeObserver.disconnect(); + if (this.referenceAutocomplete) { this.referenceAutocomplete.destroy(); } @@ -652,6 +663,8 @@ export class ChatPanel { // Обработчики Drag-n-Drop для внешних файлов this.setupDragAndDrop(); + + this.setupScrollObservers(); } // ----------------------------------------------- Reference Processing --------------------------------------------------------------- @@ -1360,4 +1373,64 @@ export class ChatPanel { new Notice(`Прикреплено файлов: ${addedCount}`, 2000); } } + + // ----------------------------------------------- Scroll Observers --------------------------------------------------------------- + + /** + * Настраивает наблюдатели за размерами окна и внутренним контентом чата + */ + private setupScrollObservers(): void { + const historyContainer = this.container.querySelector('#chat-history') as HTMLElement; + this.cachedScrollContainer = this.container.closest('.view-content') as HTMLElement; + this.cachedBtnWrapper = this.container.querySelector('.sticky-btn-wrapper') as HTMLElement; + + if (!this.cachedScrollContainer || !this.cachedBtnWrapper) return; + + // Оставляем только ResizeObserver. + this.resizeObserver = new ResizeObserver(() => { + this.scheduleDebouncedScrollUpdate(); + }); + + // Следим за родителем (ресайз панелей) + this.resizeObserver.observe(this.cachedScrollContainer); + + // Следим за контентом (чтобы поймать появление overlay-скроллбаров на Mac) + if (historyContainer) { + this.resizeObserver.observe(historyContainer); + } + + // Проверяем состояние при инициализации + setTimeout(() => this.updateScrollButtonsVisibility(), 0); + } + + /** + * Схлопывает частые вызовы событий (debounce) + */ + private scheduleDebouncedScrollUpdate(): void { + if (this.scrollDebounceTimer !== null) { + window.clearTimeout(this.scrollDebounceTimer); + } + + // Запускаем перерасчет только если изменения "затихли" на 200мс + this.scrollDebounceTimer = window.setTimeout(() => { + this.updateScrollButtonsVisibility(); + this.scrollDebounceTimer = null; + }, 200); + } + + /** + * Проверяет наличие скроллбара на родительском элементе и скрывает/показывает кнопки + */ + private updateScrollButtonsVisibility(): void { + if (!this.cachedScrollContainer || !this.cachedBtnWrapper) return; + + // DOM больше не сканируется через closest/querySelector, элементы закэшированы + const hasScrollbar = this.cachedScrollContainer.scrollHeight > (this.cachedScrollContainer.clientHeight + 2); + + if (hasScrollbar) { + this.cachedBtnWrapper.classList.add('is-visible'); + } else { + this.cachedBtnWrapper.classList.remove('is-visible'); + } + } } diff --git a/src/css/styles.css b/src/css/styles.css index e7c52e3..9edcc08 100644 --- a/src/css/styles.css +++ b/src/css/styles.css @@ -419,7 +419,7 @@ } .stop-button, .submit-button { - padding: 4px 8px; /* Восстанавливаем паддинг */ + padding: 4px 10px; /* Восстанавливаем паддинг */ border: none; /* Убираем рамку */ border-radius: 8px; cursor: pointer; @@ -518,6 +518,17 @@ width: 100%; pointer-events: none; flex-shrink: 0; + + /* Скрываем по умолчанию */ + opacity: 0; + visibility: hidden; + transition: opacity 0.3s ease, visibility 0.3s ease; +} + +/* Класс, который добавляется JS, когда есть скролл */ +.sticky-btn-wrapper.is-visible { + opacity: 1; + visibility: visible; } /* Добавить общий стиль для обеих кнопок и специфичный для новой */