Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,15 @@ The wordmark should use the aqua brand color.

Everything below the wordmark should stay concise.

The welcome brand signal is framed in a single aqua panel. Wide terminals show
the DevMap symbol above the large wordmark. Narrow terminals keep the symbol
but use a compact `DEVMAP` title so the panel does not wrap or clip.
The welcome brand signal uses a large outlined block DevMap wordmark
without a surrounding panel or separate symbol. A small CLI label, concise
capability line, and solid aqua separator make it feel like a professional
developer tool without adding decorative terminal noise.

Narrow terminals use a compact `DEVMAP` title and shorter capability label so
the welcome header does not wrap or clip. The wide wordmark relies on Unicode
block and box-drawing glyphs; terminals without compatible fonts may use the
compact fallback or require a font configuration adjustment.

Example:

Expand Down
11 changes: 7 additions & 4 deletions docs/for-me-personal/PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ Terakhir diperbarui: 2026-06-14

### Welcome Brand Panel

- Welcome screen sekarang menampilkan simbol logo DevMap di atas ASCII
wordmark.
- Logo, wordmark, dan border memakai warna aqua brand yang sama.
- Terminal lebar memakai wordmark penuh dalam panel maksimal 76 kolom.
- Simbol logo terpisah dan border luar dihapus agar welcome screen lebih bersih.
- Terminal lebar memakai wordmark blok berpinggiran seperti identitas awal.
- Label `DEVMAP CLI`, capability line, dan separator aqua solid memperjelas
identitas produk sebagai developer tool.
- Perbedaan render glyph Unicode pada sebagian font terminal dicatat sebagai
known issue untuk compatibility pass berikutnya.
- Terminal lebar memakai wordmark penuh dalam area maksimal 76 kolom.
- Terminal sempit memakai judul `DEVMAP` ringkas agar panel tidak wrap atau
terpotong.
- Renderer panel dipisahkan agar layout lebar dan sempit dapat diuji langsung.
Expand Down
65 changes: 31 additions & 34 deletions packages/cli/src/utils/welcome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,6 @@ import { existsSync } from "node:fs";
import { getSnapshotPath } from "../cache/snapshot.js";
import { theme } from "./output.js";

const SYMBOL_LOGO = [
" ╷ ╱╲",
" ╭──────┼──────╱ ╲",
" ╭─╯ ●─────╯ ╱",
" ╱ ╱│ ╱",
"│ ╱ │ ╭────╯",
"│ ╱ │╱╱",
" ╲__╱ ╱│",
" ╲____╱ │",
" ╵"
];

const WIDE_WORDMARK = [
"██████╗ ███████╗██╗ ██╗███╗ ███╗ █████╗ ██████╗",
"██╔══██╗██╔════╝██║ ██║████╗ ████║██╔══██╗██╔══██╗",
Expand All @@ -26,6 +14,8 @@ const WIDE_WORDMARK = [
const WIDE_PANEL_WIDTH = 76;
const COMPACT_PANEL_WIDTH = 48;
const WIDE_TERMINAL_MINIMUM = 72;
const PRODUCT_LABEL = "DEVMAP CLI";
const PRODUCT_CAPABILITIES = "CODEBASE MAP / STATIC ANALYSIS / AI CONTEXT";

export function printWelcome(projectRoot: string): void {
const hasSnapshot = existsSync(getSnapshotPath(projectRoot));
Expand All @@ -49,32 +39,39 @@ export function printWelcome(projectRoot: string): void {

export function renderWelcomeBrandPanel(terminalWidth: number): string {
const isWide = terminalWidth >= WIDE_TERMINAL_MINIMUM;
const panelWidth = Math.max(
28,
Math.min(
terminalWidth,
isWide ? WIDE_PANEL_WIDTH : COMPACT_PANEL_WIDTH
)
const contentWidth = Math.max(
24,
Math.min(terminalWidth, isWide ? WIDE_PANEL_WIDTH : COMPACT_PANEL_WIDTH)
);
const contentWidth = panelWidth - 4;
const wordmark = isWide ? WIDE_WORDMARK : ["DEVMAP"];
const content = [
...SYMBOL_LOGO,
const label = centerLine(`[ ${PRODUCT_LABEL} ]`, contentWidth);
const wordmarkRows = centerBlock(wordmark, contentWidth);
const capabilities = centerLine(
isWide ? PRODUCT_CAPABILITIES : "CODEBASE INTELLIGENCE",
contentWidth
);
const separator = "━".repeat(Math.min(contentWidth, 64));

return [
`${theme.gray}${label}${theme.reset}`,
"",
...wordmark
];
const top = `╭${"─".repeat(panelWidth - 2)}╮`;
const bottom = `╰${"─".repeat(panelWidth - 2)}╯`;
const rows = content.map((line) => {
const visibleLine = line.slice(0, contentWidth);
const leftPadding = Math.floor((contentWidth - visibleLine.length) / 2);
const rightPadding = contentWidth - visibleLine.length - leftPadding;
return `│ ${" ".repeat(leftPadding)}${visibleLine}${" ".repeat(rightPadding)} │`;
});
...wordmarkRows.map((line) => `${theme.aqua}${line}${theme.reset}`),
"",
`${theme.gray}${capabilities}${theme.reset}`,
`${theme.aqua}${separator}${theme.reset}`
].join("\n");
}

function centerLine(line: string, width: number): string {
const visibleLine = line.slice(0, width);
const leftPadding = Math.floor((width - visibleLine.length) / 2);
return `${" ".repeat(leftPadding)}${visibleLine}`;
}

return [top, ...rows, bottom]
.map((line) => `${theme.aqua}${line}${theme.reset}`)
.join("\n");
function centerBlock(lines: string[], width: number): string[] {
const blockWidth = Math.min(width, Math.max(...lines.map((line) => line.length)));
const leftPadding = Math.floor((width - blockWidth) / 2);
return lines.map((line) => `${" ".repeat(leftPadding)}${line.slice(0, width)}`);
}

function printCommand(command: string, description?: string): void {
Expand Down
23 changes: 13 additions & 10 deletions packages/cli/test/welcome.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,29 @@ import assert from "node:assert/strict";
import test from "node:test";
import { renderWelcomeBrandPanel } from "../src/utils/welcome.js";

test("welcome brand panel frames the logo and wordmark on wide terminals", () => {
test("welcome brand panel renders an outlined block wordmark and tool identity", () => {
const panel = stripAnsi(renderWelcomeBrandPanel(100));
const lines = panel.split("\n");

assert.match(panel, /\[ DEVMAP CLI \]/);
assert.match(panel, /██████╗ ███████╗/);
assert.match(panel, /╱╲/);
assert.ok(lines.every((line) => line.length === lines[0]?.length));
assert.ok((lines[0]?.length ?? 0) <= 76);
assert.match(lines[0] ?? "", /^╭─+╮$/);
assert.match(lines.at(-1) ?? "", /^╰─+╯$/);
assert.match(panel, /██╔══██╗██╔════╝/);
assert.match(panel, /CODEBASE MAP \/ STATIC ANALYSIS \/ AI CONTEXT/);
assert.ok(lines.every((line) => line.length <= 76));
assert.match(lines.at(-1) ?? "", /^━+$/);
assert.equal(lines[2]?.indexOf("█"), lines[3]?.indexOf("█"));
});

test("welcome brand panel uses a compact wordmark on narrow terminals", () => {
test("welcome brand panel uses a compact tool identity on narrow terminals", () => {
const panel = stripAnsi(renderWelcomeBrandPanel(48));
const lines = panel.split("\n");

assert.match(panel, /\[ DEVMAP CLI \]/);
assert.match(panel, /DEVMAP/);
assert.ok(lines.every((line) => line.length === lines[0]?.length));
assert.ok((lines[0]?.length ?? 0) <= 48);
assert.doesNotMatch(panel, /██████/);
assert.match(panel, /CODEBASE INTELLIGENCE/);
assert.ok(lines.every((line) => line.length <= 48));
assert.doesNotMatch(panel, /██████╗/);
assert.match(lines.at(-1) ?? "", /^━+$/);
});

function stripAnsi(value: string): string {
Expand Down
Loading