diff --git a/src/components/BrandBanner.tsx b/src/components/BrandBanner.tsx index 7fc38bf33..dfb84aaa8 100644 --- a/src/components/BrandBanner.tsx +++ b/src/components/BrandBanner.tsx @@ -10,6 +10,10 @@ const LOGO: string[] = [ "▀ ▀ ▀▀▀ ▀▀▀ ▀ ▀ ▀ ▀▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀▀▀ ▀▀▀ ▀▀▀", ]; +export const MIN_BANNER_COLUMNS = 80; +export const MIN_BANNER_ROWS = 30; +export const BRAND_BANNER_ROWS = LOGO.length + 1; + export interface BrandBannerProps { terminalProgram?: string; theme?: InkUITheme; diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 8bf012f79..0b8e5854e 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Box } from "ink"; +import { Box, useBoxMetrics, type DOMElement } from "ink"; import { useWindowSize } from "./ui/useWindowSize"; import { Header, type HeaderProps } from "./Header"; import { Footer } from "./Footer"; @@ -8,6 +8,13 @@ import type { KeyHintProps } from "./ui/key-hint"; export interface LayoutProps { // banner is content rendered above the standard breadcrumb header. banner?: React.ReactNode; + // bannerHeight is the number of terminal rows occupied by the banner. + bannerHeight?: number; + // hideBanner suppresses the banner regardless of terminal dimensions. + hideBanner?: boolean; + // bannerMinColumns/bannerMinRows suppress the banner below these dimensions. + bannerMinColumns?: number; + bannerMinRows?: number; // breadcrumb is passed through to the Header. breadcrumb: HeaderProps["breadcrumb"]; // description is passed through to the Header, shown dimmed after the breadcrumb. @@ -15,27 +22,56 @@ export interface LayoutProps { // keyHints is passed through to the Footer (KeyHint) row. keyHints: KeyHintProps["keys"]; // children fill the content area between the header and footer. - children: React.ReactNode; + children: + | React.ReactNode + | ((dimensions: { + columns: number; + rows: number; + contentRows: number; + bannerVisible: boolean; + }) => React.ReactNode); } +const DEFAULT_HEADER_ROWS = 2; +const FOOTER_ROWS = 2; + // Layout is the standard full-screen frame: a breadcrumb Header at the top, a // KeyHint Footer at the bottom, and a flexible content area in between that grows // to fill the remaining terminal height. export const Layout: React.FC = ({ banner, + bannerHeight = 0, + hideBanner = false, + bannerMinColumns = 0, + bannerMinRows = 0, breadcrumb, description, keyHints, children, }) => { const { columns, rows } = useWindowSize(); + const headerRef = React.useRef(null); + const { height: measuredHeaderRows, hasMeasured: hasMeasuredHeader } = useBoxMetrics(headerRef); + const bannerVisible = + Boolean(banner) && !hideBanner && columns >= bannerMinColumns && rows >= bannerMinRows; + const headerRows = hasMeasuredHeader ? measuredHeaderRows : DEFAULT_HEADER_ROWS; + const contentRows = Math.max( + 0, + rows - headerRows - FOOTER_ROWS - (bannerVisible ? bannerHeight : 0), + ); + const content = + typeof children === "function" + ? children({ columns, rows, contentRows, bannerVisible }) + : children; return ( - {banner} -
+ {bannerVisible ? banner : null} + +
+ - {children} + {content}