Files
stack-mos-dt-0/packages/mosaic/src/tui/hooks/use-viewport.ts
T
jarvis c6fc090c98 feat(mosaic): merge @mosaic/cli into @mosaic/mosaic
@mosaic/mosaic is now the single package providing both:
- 'mosaic' binary (CLI: yolo, coord, prdy, tui, gateway, etc.)
- 'mosaic-wizard' binary (installation wizard)

Changes:
- Move packages/cli/src/* into packages/mosaic/src/
- Convert dynamic @mosaic/mosaic imports to static relative imports
- Add CLI deps (ink, react, socket.io-client, @mosaic/config) to mosaic
- Add jsx: react-jsx to mosaic's tsconfig
- Exclude packages/cli from workspace (pnpm-workspace.yaml)
- Update install.sh to install @mosaic/mosaic instead of @mosaic/cli
- Bump version to 0.0.17

This eliminates the circular dependency between @mosaic/cli and
@mosaic/mosaic that was blocking the build graph.
2026-04-04 20:07:27 -05:00

81 lines
2.0 KiB
TypeScript

import { useState, useCallback, useEffect } from 'react';
import { useStdout } from 'ink';
export interface UseViewportOptions {
totalItems: number;
reservedLines?: number;
}
export interface UseViewportReturn {
scrollOffset: number;
viewportSize: number;
isScrolledUp: boolean;
scrollToBottom: () => void;
scrollBy: (delta: number) => void;
scrollTo: (offset: number) => void;
canScrollUp: boolean;
canScrollDown: boolean;
}
export function useViewport({
totalItems,
reservedLines = 10,
}: UseViewportOptions): UseViewportReturn {
const { stdout } = useStdout();
const rows = stdout?.rows ?? 24;
const viewportSize = Math.max(1, rows - reservedLines);
const [scrollOffset, setScrollOffset] = useState(0);
const [autoFollow, setAutoFollow] = useState(true);
// Compute the maximum valid scroll offset
const maxOffset = Math.max(0, totalItems - viewportSize);
// Auto-follow: when new items arrive and auto-follow is on, snap to bottom
useEffect(() => {
if (autoFollow) {
setScrollOffset(maxOffset);
}
}, [autoFollow, maxOffset]);
const scrollTo = useCallback(
(offset: number) => {
const clamped = Math.max(0, Math.min(offset, maxOffset));
setScrollOffset(clamped);
setAutoFollow(clamped >= maxOffset);
},
[maxOffset],
);
const scrollBy = useCallback(
(delta: number) => {
setScrollOffset((prev) => {
const next = Math.max(0, Math.min(prev + delta, maxOffset));
setAutoFollow(next >= maxOffset);
return next;
});
},
[maxOffset],
);
const scrollToBottom = useCallback(() => {
setScrollOffset(maxOffset);
setAutoFollow(true);
}, [maxOffset]);
const isScrolledUp = scrollOffset < maxOffset;
const canScrollUp = scrollOffset > 0;
const canScrollDown = scrollOffset < maxOffset;
return {
scrollOffset,
viewportSize,
isScrolledUp,
scrollToBottom,
scrollBy,
scrollTo,
canScrollUp,
canScrollDown,
};
}