MagicCode/packages/ui/src/banner.tsx
2026-10-16 00:54:08 +02:00

98 lines
2.7 KiB
TypeScript

import React, { useMemo } from "react";
import { Box, Text, useStdout } from "ink";
import { rainbowColors } from "./colors";
import {
MASK_WIDTH,
buildMaskGrid,
buildTitleGrid,
canRenderTitle,
pixelsToRuns,
titleWidth,
type Run,
} from "./pixels";
const GREEN = "#7BD74F";
const PINK = "#E58AFF";
const GREEN_LINES = ["Assistant IA en ligne de commande,", "propulsé par Ink et vos modèles de langage."];
const PINK_LINES = ["Un seul agent, plusieurs modèles"];
const MASK_LINES = pixelsToRuns(buildMaskGrid());
const GAP = 3;
function PixelBlock({ lines }: { lines: Run[][] }) {
return (
<Box flexDirection="column">
{lines.map((runs, y) => (
<Text key={y}>
{runs.map((r, i) => (
<Text key={i} color={r.fg} backgroundColor={r.bg}>
{r.text}
</Text>
))}
</Text>
))}
</Box>
);
}
/** Repli pour un terminal étroit : cadre à double bordure, titre centré en dégradé arc-en-ciel. */
function CompactBanner({ title, columns }: { title: string; columns: number }) {
const chars = [...title];
const colors = rainbowColors(chars.length);
return (
<Box borderStyle="double" justifyContent="center" paddingX={1} width={Math.max(columns, chars.length + 6)}>
<Text bold>
{chars.map((ch, i) => (
<Text key={i} color={colors[i]}>
{ch}
</Text>
))}
</Text>
</Box>
);
}
/**
* Bannière d'accueil : masque pixel-art à gauche, titre pixel arc-en-ciel à droite, puis deux textes
* (vert, rose). Selon la largeur du terminal : complète, sans masque, ou cadre compact.
*/
export function Banner({ title }: { title: string }) {
const { stdout } = useStdout();
const columns = stdout?.columns ?? 80;
const titleLines = useMemo(
() => (canRenderTitle(title) ? pixelsToRuns(buildTitleGrid(title)) : null),
[title],
);
if (!titleLines || columns < titleWidth(title) + 2) {
return <CompactBanner title={title} columns={columns} />;
}
const right = (
<Box flexDirection="column">
<PixelBlock lines={titleLines} />
<Box marginTop={1} flexDirection="column">
{GREEN_LINES.map((line) => (
<Text key={line} color={GREEN} wrap="truncate">
{line}
</Text>
))}
</Box>
<Box marginTop={1} flexDirection="column">
{PINK_LINES.map((line) => (
<Text key={line} color={PINK} wrap="truncate">
{line}
</Text>
))}
</Box>
</Box>
);
if (columns < MASK_WIDTH + GAP + titleWidth(title)) return right;
return (
<Box alignItems="center">
<PixelBlock lines={MASK_LINES} />
<Box marginLeft={GAP}>{right}</Box>
</Box>
);
}