import { createEffect, For, createMemo, createSignal, onCleanup, onMount } from "solid-js";
import { RGBA } from "@opentui/core";
import { useRenderer, useTerminalDimensions } from "@opentui/solid";
import { useTheme } from "@tui/context/theme";
import { useKV } from "@tui/context/kv";
import {
bannerLogoPalette,
bannerLogoScannedLineTonesWithIntro,
LOGO_INTRO_DURATION_MS,
LOGO_ROW_CAP,
logoIntroFrameAt,
logoRowsForWidth,
thickTopLogoRows,
type LogoIntroFrame,
type Tone,
} from "./banner-logo";
* DevEco Code home banner.
*
* - Renders a fixed-height (8-row) ANSI lettermark using parsed SGR spans (supports 256-color + truecolor).
* - Logo: full "DEVECO CODE" when the terminal is wide enough; otherwise "DEVECO"; if still too narrow,
* the lettermark is left-aligned and truncated (see `banner-logo.ts`).
* - Adds a scanline effect by replacing spaces with `─`.
* - On mount: ~4.4s intro — block scan → pause → row reveal → pause → shift R/L → center → final mark.
* - Taglines below are centered via left-padding (reliable in terminal layouts) and may use per-character gradient.
*/
export const BANNER_HOME_CONTENT_INSET = 0;
export const HOME_CONTENT_MAX_WIDTH = 110;
export const HOME_CONTENT_PAD_X_MAX = 15;
* Home onboarding horizontal padding; shrinks on narrow terminals so copy keeps usable width.
* Wide: aligns under centered banner tagline; narrow: minimal side margin.
*/
export function homeContentPadX(terminalWidth: number): number {
const w = Math.max(0, Math.floor(terminalWidth));
if (w >= 110) return HOME_CONTENT_PAD_X_MAX;
if (w >= 90) return 12;
if (w >= 72) return 8;
if (w >= 56) return 4;
if (w >= 40) return 2;
return 0;
}
export const HOME_BODY_MAX_ROWS = 18;
export const HOME_BODY_MIN_ROWS = HOME_BODY_MAX_ROWS;
export const HOME_BODY_SLOT_FLOOR_ROWS = 8;
export const HOME_BODY_GAP_ROWS = 1;
export const HOME_BANNER_ESTIMATE_ROWS = 1 + LOGO_ROW_CAP + 2 + 2;
export const HOME_LAYOUT_FOOTER_ROWS = 2;
export const HOME_LAYOUT_MARGIN_ROWS = 2;
export function homeBodySlotRows(terminalHeight: number): number {
const h = Math.max(0, Math.floor(terminalHeight));
const reserved =
HOME_BANNER_ESTIMATE_ROWS + HOME_BODY_GAP_ROWS + HOME_LAYOUT_FOOTER_ROWS + HOME_LAYOUT_MARGIN_ROWS;
const available = h - reserved;
if (available <= HOME_BODY_SLOT_FLOOR_ROWS) {
return Math.max(4, available);
}
return Math.min(HOME_BODY_MAX_ROWS, available);
}
export const HOME_PROMPT_MAX_TEXTAREA_ROWS = 4;
export const HOME_PROMPT_MIN_TEXTAREA_ROWS = 2;
export const HOME_PROMPT_CHROME_ROWS = 3;
export const HOME_PROMPT_TIPS_RESERVE_ROWS = 4;
export function homePromptTextareaRows(
bodySlotHeight: number,
tipsReserveRows: number = HOME_PROMPT_TIPS_RESERVE_ROWS,
): number {
const inner = Math.max(0, bodySlotHeight - HOME_BODY_GAP_ROWS);
const available = inner - HOME_PROMPT_CHROME_ROWS - Math.max(0, tipsReserveRows);
if (available <= HOME_PROMPT_MIN_TEXTAREA_ROWS) {
return Math.max(1, available);
}
return Math.min(HOME_PROMPT_MAX_TEXTAREA_ROWS, available);
}
export function Banner(props?: { contentInset?: number }) {
const { theme, mode } = useTheme();
const kv = useKV();
const renderer = useRenderer();
const dimensions = useTerminalDimensions();
const animationsEnabled = () => kv.get("animations_enabled", true);
const [introFrame, setIntroFrame] = createSignal<LogoIntroFrame>(
animationsEnabled()
? logoIntroFrameAt(0, LOGO_ROW_CAP)
: { kind: "done" },
);
const width = createMemo(() => {
const inset = props?.contentInset ?? 0;
return Math.max(0, Math.floor(dimensions().width) - inset);
});
const logoRows = createMemo(() => logoRowsForWidth(width()).slice(0, LOGO_ROW_CAP));
const logoRowsThickTop = createMemo(() => thickTopLogoRows(logoRows()));
const isLight = createMemo(() => mode() === "light");
const logoPalette = createMemo(() => bannerLogoPalette(isLight(), theme));
createEffect(() => {
introFrame();
renderer.requestRender();
});
onMount(() => {
if (!animationsEnabled()) return;
const rows = logoRows().length;
const fadeStart = performance.now();
const interval = setInterval(() => {
const elapsed = performance.now() - fadeStart;
setIntroFrame(logoIntroFrameAt(elapsed, rows));
if (elapsed >= LOGO_INTRO_DURATION_MS) clearInterval(interval);
}, 16);
onCleanup(() => clearInterval(interval));
});
const bannerPalette = createMemo(() => {
if (!isLight()) {
return {
gradLo: { r: 141, g: 143, b: 255 },
gradHi: { r: 179, g: 133, b: 236 },
};
}
return {
gradLo: { r: 62, g: 64, b: 148 },
gradHi: { r: 108, g: 58, b: 138 },
};
});
const stripeTransparent = RGBA.fromInts(0, 0, 0, 0);
const rule = createMemo(() => (width() <= 0 ? "" : "─".repeat(width())));
const lerpInt = (a: number, b: number, t: number) => Math.round(a + (b - a) * t);
const gradientAt = (i: number, steps: number) => {
const { gradLo, gradHi } = bannerPalette();
if (steps <= 1) return RGBA.fromInts(gradLo.r, gradLo.g, gradLo.b);
const t = i / (steps - 1);
return RGBA.fromInts(
lerpInt(gradLo.r, gradHi.r, t),
lerpInt(gradLo.g, gradHi.g, t),
lerpInt(gradLo.b, gradHi.b, t),
);
};
const taglineA = "Collaborate with ";
const taglineB = "DevEco Code.";
const taglineC = " An open-source AI agent for HarmonyOS application development";
const taglineLen = taglineA.length + taglineB.length + taglineC.length;
const taglinePadLeft = createMemo(() => Math.max(0, Math.floor((width() - taglineLen) / 2)));
const poweredBy = "Powered by BITFUN & OpenCode";
const poweredByLen = poweredBy.length;
const poweredByPadLeft = createMemo(() => Math.max(0, Math.floor((width() - poweredByLen) / 2)));
return (
<box flexDirection="column" width={width()} backgroundColor={stripeTransparent}>
<box flexDirection="column" width={width()} paddingTop={1} backgroundColor={stripeTransparent}>
<For each={logoRows()}>
{(line, rowIndex) => (
<text bg={stripeTransparent} selectable={false}>
<For
each={bannerLogoScannedLineTonesWithIntro(
line,
logoRowsThickTop()[rowIndex()] ?? line,
rowIndex(),
introFrame(),
width(),
logoPalette(),
)}
>
{(p: Tone) => <span style={{ fg: p.fg }}>{p.t}</span>}
</For>
</text>
)}
</For>
</box>
<box width={width()} paddingTop={1}>
<text bg={stripeTransparent} selectable={false} wrapMode="none">
<span style={{ fg: theme.textMuted }}>{`${" ".repeat(taglinePadLeft())}${taglineA}`}</span>
<For each={[...taglineB]}>
{(ch, i) => <span style={{ fg: gradientAt(i(), taglineB.length) }}>{ch}</span>}
</For>
<span style={{ fg: theme.textMuted }}>{taglineC}</span>
</text>
</box>
<box width={width()}>
<text bg={stripeTransparent} selectable={false} wrapMode="none">
<span style={{ fg: theme.text }}>{`${" ".repeat(poweredByPadLeft())}${poweredBy}`}</span>
</text>
</box>
</box>
);
}