diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index f6d39b7d3..2b3cf6838 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -13,6 +13,7 @@ import { fileURLToPath } from "node:url"; import { execFileSync, spawn } from "node:child_process"; import * as clack from "@clack/prompts"; import { c } from "../ui/colors.js"; +import { printBanner } from "../ui/banner.js"; import { TEMPLATES, type TemplateId } from "../templates/generators.js"; import { trackInitTemplate } from "../telemetry/events.js"; import { hasFFmpeg } from "../whisper/manager.js"; @@ -635,6 +636,7 @@ Examples: // ----------------------------------------------------------------------- // Interactive mode // ----------------------------------------------------------------------- + printBanner(); clack.intro("Create a new HyperFrames project"); // 1. Project name diff --git a/packages/cli/src/ui/banner.ts b/packages/cli/src/ui/banner.ts new file mode 100644 index 000000000..dc60c9000 --- /dev/null +++ b/packages/cli/src/ui/banner.ts @@ -0,0 +1,69 @@ +import { isColorSupported } from "./colors.js"; + +// --------------------------------------------------------------------------- +// Gradient stops: white → teal #74E1B9 → cyan #6ADCFF +// --------------------------------------------------------------------------- + +const GRADIENT: [number, number, number][] = [ + [255, 255, 255], // white + [116, 225, 185], // #74E1B9 teal + [106, 220, 255], // #6ADCFF cyan +]; + +function lerp(a: number, b: number, t: number): number { + return Math.round(a + (b - a) * t); +} + +function gradientColor(t: number): [number, number, number] { + const segments = GRADIENT.length - 1; + const scaled = t * segments; + const i = Math.min(Math.floor(scaled), segments - 1); + const frac = scaled - i; + const a = GRADIENT[i]!; + const b = GRADIENT[i + 1]!; + return [lerp(a[0], b[0], frac), lerp(a[1], b[1], frac), lerp(a[2], b[2], frac)]; +} + +function rgb(r: number, g: number, b: number, text: string): string { + return `\x1b[38;2;${r};${g};${b}m${text}\x1b[0m`; +} + +// --------------------------------------------------------------------------- +// Box-drawing block art (figlet "ANSI Shadow" style) +// --------------------------------------------------------------------------- + +const BANNER = [ + "\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557", + "\u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D", + "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557", + "\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551 \u255A\u2588\u2588\u2554\u255D \u2588\u2588\u2554\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551", + "\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551", + "\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D", +]; + +// --------------------------------------------------------------------------- +// Public +// --------------------------------------------------------------------------- + +export function printBanner(): void { + if (!isColorSupported || !process.stdout.isTTY) return; + + const maxWidth = Math.max(...BANNER.map((r) => r.length)); + + console.log(); + for (const row of BANNER) { + let line = " "; + for (let col = 0; col < row.length; col++) { + const ch = row[col]; + if (!ch || ch === " ") { + line += " "; + continue; + } + const t = maxWidth > 1 ? col / (maxWidth - 1) : 0; + const [r, g, b] = gradientColor(t); + line += rgb(r, g, b, ch); + } + console.log(line); + } + console.log(); +}