// Pure, DOM-free scroll-decision logic for MessageList. Unit-tested so the // tricky math is verified without a browser (jsdom has no layout). export interface ScrollMetrics { scrollTop: number; scrollHeight: number; clientHeight: number; } /** Distance from the bottom edge is within `threshold` px. */ export function isNearBottom(m: ScrollMetrics, threshold: number): boolean { return m.scrollHeight - (m.scrollTop + m.clientHeight) <= threshold; } /** Scroll offset is within `threshold` px of the top. */ export function isNearTop(m: ScrollMetrics, threshold: number): boolean { return m.scrollTop <= threshold; } export interface SavedPosition { topmostIndex: number; stickToBottom: boolean; } export interface Anchor { index: number; align: 'start' | 'end'; } /** * Where a freshly-opened chat should start. * - default / "left at bottom" → last row, aligned to the viewport bottom. * - "left scrolled up" → the saved top-most row, aligned to the viewport top * (clamped in case the cached row count shrank). */ export function resolveInitialAnchor(saved: SavedPosition | null, rowCount: number): Anchor { if (rowCount <= 0) return { index: 0, align: 'end' }; if (saved && !saved.stickToBottom) { const index = Math.max(0, Math.min(saved.topmostIndex, rowCount - 1)); return { index, align: 'start' }; } return { index: rowCount - 1, align: 'end' }; }