forked from mosaicstack/stack
@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.
81 lines
2.0 KiB
TypeScript
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,
|
|
};
|
|
}
|