diff --git a/PRODUCT.md b/PRODUCT.md index 689de6c5b..36f74456d 100644 --- a/PRODUCT.md +++ b/PRODUCT.md @@ -78,11 +78,14 @@ no hosted runtime to sell. - Voice: quiet, dense, factual. Terminal vocabulary, no marketing superlatives, no fabricated transcripts or reasoning traces. - "It doesn't need to look special — it needs to look like Codewhale." -- Assets: the founder's whale mark traced to `brand/mark.svg` (with - `brand/mark-navy.svg`, `brand/mark-gradient.svg`) and the founder's rounded - monoline wordmark traced from `brand/wordmark0901.png` (`brand/wordmark.svg` - navy #142352, `brand/wordmark-inverted.svg` white; regenerated with the site - icons by `scripts/brand/trace-brand.py`). Web copies live in +- Assets: the canonical vector family lives in the CWC repo at + `codewhale-apps/packages/brand/svg/` (mark, mark-gradient, mark-mono, + mark-reversed, wordmark, wordmark-inverted); `brand/` and + `web/public/brand/` carry byte-identical copies — sync from there, never + re-trace. The founder's brand sheet `brand/codewhalemarkfinal.png` remains + the source `scripts/brand/braille-mark.py` derives TUI launch art from. The + earlier local trace and `scripts/brand/trace-brand.py` were retired + 2026-09-15 in favor of the canonical family. Web copies live in `web/public/brand/`. - Palette, type, shell direction, and the anti-slop rules are recorded in `DESIGN.md`; the colour tokens are owned by `crates/tui/src/palette/tokens.rs` diff --git a/brand/mark-gradient.svg b/brand/mark-gradient.svg index 412eb6c12..412e4c14d 100644 --- a/brand/mark-gradient.svg +++ b/brand/mark-gradient.svg @@ -1 +1,9 @@ - + + + + + + + + + diff --git a/brand/mark-mono.svg b/brand/mark-mono.svg new file mode 100644 index 000000000..f67e8d277 --- /dev/null +++ b/brand/mark-mono.svg @@ -0,0 +1,3 @@ + + + diff --git a/brand/mark-navy.svg b/brand/mark-navy.svg deleted file mode 100644 index 4272f677a..000000000 --- a/brand/mark-navy.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/brand/mark-reversed.svg b/brand/mark-reversed.svg new file mode 100644 index 000000000..90ceae43c --- /dev/null +++ b/brand/mark-reversed.svg @@ -0,0 +1,3 @@ + + + diff --git a/brand/mark.svg b/brand/mark.svg index 75b6a4d86..71d3b534c 100644 --- a/brand/mark.svg +++ b/brand/mark.svg @@ -1 +1,3 @@ - + + + diff --git a/brand/wordmark-inverted.svg b/brand/wordmark-inverted.svg index e53701417..b0bb87e19 100644 --- a/brand/wordmark-inverted.svg +++ b/brand/wordmark-inverted.svg @@ -1 +1,3 @@ - + + + diff --git a/brand/wordmark.svg b/brand/wordmark.svg index 81133ab7e..03d61e895 100644 --- a/brand/wordmark.svg +++ b/brand/wordmark.svg @@ -1 +1,3 @@ - + + + diff --git a/brand/wordmark0901.png b/brand/wordmark0901.png deleted file mode 100644 index 23f403178..000000000 Binary files a/brand/wordmark0901.png and /dev/null differ diff --git a/brand/wordmarkinverted.png b/brand/wordmarkinverted.png deleted file mode 100644 index e66c3fb32..000000000 Binary files a/brand/wordmarkinverted.png and /dev/null differ diff --git a/crates/tui/src/runtime_web/codewhale-192.png b/crates/tui/src/runtime_web/codewhale-192.png index bfb67a6ff..7513605ae 100644 Binary files a/crates/tui/src/runtime_web/codewhale-192.png and b/crates/tui/src/runtime_web/codewhale-192.png differ diff --git a/crates/tui/src/tui/views/mod.rs b/crates/tui/src/tui/views/mod.rs index 08fbf9f43..1a5626d83 100644 --- a/crates/tui/src/tui/views/mod.rs +++ b/crates/tui/src/tui/views/mod.rs @@ -10383,7 +10383,7 @@ context_window = 262144 "Enter must open the theme editor" ); - // ↓ highlights underwater: preview (persist:false), editor stays open. + // ↓ highlights shoreline: preview (persist:false), editor stays open. match key(&mut view, KeyCode::Down) { ViewAction::Emit(ViewEvent::ConfigUpdated { key, @@ -10391,7 +10391,7 @@ context_window = 262144 persist, }) => { assert_eq!(key, "theme"); - assert_eq!(value, "underwater"); + assert_eq!(value, "shoreline"); assert!(!persist, "highlighting must not persist"); } other => panic!("highlight must preview, got {other:?}"), @@ -10433,7 +10433,7 @@ context_window = 262144 persist, }) => { assert_eq!(key, "theme"); - assert_eq!(value, "underwater"); + assert_eq!(value, "shoreline"); assert!(persist, "Apply must persist"); } other => panic!("enter must persist the highlight, got {other:?}"), @@ -10508,14 +10508,14 @@ context_window = 262144 modifiers: KeyModifiers::NONE, }) }; - // Choice index 2 is underwater (system, terminal, underwater, …). + // Choice index 2 is shoreline (system, terminal, shoreline, …). let (rect, _) = view .last_choice_hitboxes .borrow() .iter() .copied() .find(|(_, idx)| *idx == 2) - .expect("rendered underwater hitbox"); + .expect("rendered shoreline hitbox"); match hover(&mut view, rect.x, rect.y) { ViewAction::Emit(ViewEvent::ConfigUpdated { key, @@ -10523,7 +10523,7 @@ context_window = 262144 persist, }) => { assert_eq!(key, "theme"); - assert_eq!(value, "underwater"); + assert_eq!(value, "shoreline"); assert!(!persist, "hover preview must not persist"); } other => panic!("hover must preview, got {other:?}"), diff --git a/extensions/vscode/media/codewhale.svg b/extensions/vscode/media/codewhale.svg index a082e2bc5..f67e8d277 100644 --- a/extensions/vscode/media/codewhale.svg +++ b/extensions/vscode/media/codewhale.svg @@ -1,11 +1,3 @@ - - + diff --git a/scripts/brand/trace-brand.py b/scripts/brand/trace-brand.py deleted file mode 100644 index 054f3d5c9..000000000 --- a/scripts/brand/trace-brand.py +++ /dev/null @@ -1,233 +0,0 @@ -#!/usr/bin/env python3 -"""Re-derive every brand surface from the founder's artwork in brand/. - - python3 scripts/brand/trace-brand.py # rewrite every output - python3 scripts/brand/trace-brand.py --check # exit 1 if any output drifted - -Founder sources (committed, never regenerated): - - brand/codewhalemarkfinal.png 1254x1254 sheet; hero mark top centre - brand/wordmark0901.png "codewhale" wordmark, navy on white, 2172x724 - brand/wordmarkinverted.png the same wordmark, white on navy, 2508x627 - -`brand/mark.svg` is the kept trace of the sheet's hero mark (viewBox 0 0 512 -512, one evenodd path, fill currentColor). It is the source of truth for the -mark: this script does not re-trace it, it derives every other mark surface -from it. The wordmark is traced from `wordmark0901.png` every run: - - magick brand/wordmark0901.png -colorspace Gray -threshold 60% -trim +repage wm.pbm - potrace wm.pbm -s --flat -t 20 -O 0.4 -a 1.2 -o wm-raw.svg - -potrace's `translate(0,H) scale(0.1,-0.1)` group is folded into the -coordinates so the result is one compact path in a tight pixel viewBox -(1874x264 for the 0901 render; the wordmark is ~7.1:1). - -`wordmarkinverted.png` traces just as cleanly (negate first, threshold 40%), -but its render is set slightly tighter (2172x310 after trim, ~7.0:1), so the -0901 render is the one path used for both colourways: geometry that differs -between the light and dark wordmark is a second wordmark. - -Outputs: - - brand/wordmark.svg #142352 on transparent - brand/wordmark-inverted.svg #FFFFFF on transparent, same path - brand/mark-navy.svg mark.svg path, #142352 - brand/mark-gradient.svg mark.svg path, cobalt->action gradient - web/public/brand/{mark,mark-gradient,wordmark,wordmark-inverted}.svg - web/app/icon.svg white mark on the #142352 rounded tile - web/app/favicon.ico 16/32/48 of the tile - web/app/apple-icon.png 180, opaque tile - web/public/icon-192.png, icon-512.png - -`web/components/whale.tsx` inlines the mark path; the script checks it still -matches `brand/mark.svg` rather than rewriting a React component. - -Needs ImageMagick 7 (`magick`) and potrace on PATH. No network. -""" - -from __future__ import annotations - -import re -import shutil -import subprocess -import sys -import tempfile -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -BRAND = ROOT / "brand" -WEB = ROOT / "web" - -NAVY = "#142352" -WHITE = "#FFFFFF" -# Gradient stops are the TUI's WHALE_COBALT -> WHALE_ACTION. -GRADIENT = ("#1535B2", "#6AA6DC") - -# The mark in the 512 tile: the sheet's app icon sets the whale at ~2/3 of -# the tile width, so scale the 512 mark by 0.72 and centre its content box -# (x 16.9..494.3, y 46.7..471.1) on 256,256. -TILE_SCALE = 0.72 -TILE_TRANSLATE = (72, 70) -TILE_RADIUS = 102.4 # 20% of 512, the sheet's corner - -THRESHOLD = "60%" -POTRACE = ["potrace", "-s", "--flat", "-t", "20", "-O", "0.4", "-a", "1.2"] - - -def run(*args: str) -> None: - subprocess.run(args, check=True) - - -def number(value: float) -> str: - text = f"{value:.1f}" - return text[:-2] if text.endswith(".0") else text - - -def fold_potrace(svg: str) -> tuple[str, float, float]: - """Return (path d, width, height) with potrace's group transform applied.""" - width = float(re.search(r'width="([\d.]+)pt"', svg).group(1)) - height = float(re.search(r'height="([\d.]+)pt"', svg).group(1)) - tx, ty, sx, sy = ( - float(v) - for v in re.search( - r"translate\(([-\d.]+),([-\d.]+)\) scale\(([-\d.]+),([-\d.]+)\)", svg - ).groups() - ) - d = " ".join(re.findall(r' tuple[str, float, float]: - pbm = tmp / "wm.pbm" - raw = tmp / "wm-raw.svg" - run( - "magick", str(png), "-colorspace", "Gray", "-threshold", THRESHOLD, - "-trim", "+repage", str(pbm), - ) - run(*POTRACE, str(pbm), "-o", str(raw)) - return fold_potrace(raw.read_text()) - - -def wordmark_svg(d: str, width: float, height: float, fill: str) -> str: - return ( - f'' - f'\n' - ) - - -def mark_path() -> str: - return re.search(r' d="([^"]+)"', (BRAND / "mark.svg").read_text()).group(1) - - -def mark_svg(fill: str, defs: str = "") -> str: - return ( - '' - f'{defs}\n' - ) - - -def icon_svg() -> str: - tx, ty = TILE_TRANSLATE - return ( - '' - f'' - f'\n' - ) - - -def rasters(icon: Path) -> None: - run( - "magick", "-background", "none", str(icon), - "-define", "icon:auto-resize=48,32,16", str(WEB / "app/favicon.ico"), - ) - run( - "magick", "-background", NAVY, str(icon), "-resize", "180x180", - "-alpha", "off", "-depth", "8", str(WEB / "app/apple-icon.png"), - ) - for size in (192, 512): - run( - "magick", "-background", "none", str(icon), "-resize", f"{size}x{size}", - "-depth", "8", str(WEB / f"public/icon-{size}.png"), - ) - - -def main() -> int: - check = "--check" in sys.argv[1:] - gradient = ( - '' - f'' - f'' - ) - with tempfile.TemporaryDirectory() as tmpdir: - tmp = Path(tmpdir) - d, width, height = trace_wordmark(BRAND / "wordmark0901.png", tmp) - texts = { - BRAND / "wordmark.svg": wordmark_svg(d, width, height, NAVY), - BRAND / "wordmark-inverted.svg": wordmark_svg(d, width, height, WHITE), - BRAND / "mark-navy.svg": mark_svg(NAVY), - BRAND / "mark-gradient.svg": mark_svg("url(#mark-gradient)", gradient), - WEB / "app/icon.svg": icon_svg(), - } - texts[WEB / "public/brand/mark.svg"] = (BRAND / "mark.svg").read_text() - for name in ("mark-gradient", "wordmark", "wordmark-inverted"): - texts[WEB / "public/brand" / f"{name}.svg"] = texts[BRAND / f"{name}.svg"] - - whale = (WEB / "components/whale.tsx").read_text() - inline = re.search(r'WHALE_MARK =\s*"([^"]+)"', whale).group(1) - drift = [] if inline == mark_path() else ["web/components/whale.tsx"] - - if check: - drift += [ - str(path.relative_to(ROOT)) - for path, text in texts.items() - if not path.exists() or path.read_text() != text - ] - for name in drift: - print(f"drift: {name}", file=sys.stderr) - return 1 if drift else 0 - - for path, text in texts.items(): - path.write_text(text) - print(f"wrote {path.relative_to(ROOT)}") - rasters(WEB / "app/icon.svg") - print("wrote web/app/favicon.ico apple-icon.png public/icon-192.png icon-512.png") - for name in drift: - print(f"drift: {name} no longer matches brand/mark.svg", file=sys.stderr) - return 1 if drift else 0 - - -if __name__ == "__main__": - if not shutil.which("magick") or not shutil.which("potrace"): - sys.exit("needs `magick` (ImageMagick 7) and `potrace` on PATH") - sys.exit(main()) diff --git a/web/app/opengraph-image.tsx b/web/app/opengraph-image.tsx index 57f058cd9..94715aa88 100644 --- a/web/app/opengraph-image.tsx +++ b/web/app/opengraph-image.tsx @@ -7,8 +7,8 @@ export const alt = OG_ALT; export const size = { width: 1200, height: 630 }; export const contentType = "image/png"; -const mark = readFile(join(process.cwd(), "public/brand/mark.svg")).then((svg) => - svg.toString().replace("currentColor", "#ffffff"), +const mark = readFile(join(process.cwd(), "public/brand/mark-reversed.svg")).then((svg) => + svg.toString(), ); const wordmark = readFile(join(process.cwd(), "public/brand/wordmark-inverted.svg")).then( (svg) => svg.toString(), @@ -37,8 +37,8 @@ export default async function OpengraphImage() { }} > - {/* The traced wordmark is 1874x264 (~7.1:1). */} - Codewhale + {/* The family wordmark is 1024x160 (6.4:1). */} + Codewhale
{IDENTITY_PHRASE}
diff --git a/web/lib/public-auth-routes.ts b/web/lib/public-auth-routes.ts index 97d45e754..dbe4f9b2d 100644 --- a/web/lib/public-auth-routes.ts +++ b/web/lib/public-auth-routes.ts @@ -14,8 +14,9 @@ import { pathLocale } from "./i18n/path"; /** * App-icon raster (1254×1254) used on public account-entry pages: the canonical * mark in the current brand treatment — blue gradient glyph on a white rounded - * tile, generated from repo-root-relative `web/public/brand/mark.svg`. The vector is - * the master — never hand-edit this PNG, regenerate it. Pinned by hash so a + * tile. The master vectors are the canonical family in + * `codewhale-apps/packages/brand/svg/` (byte copies in `web/public/brand/`) — + * never hand-edit this PNG, regenerate it from the family. Pinned by hash so a * redrawn or stale substitute fails the test rather than shipping. */ export const CANONICAL_MARK_SRC = "/brand/codewhale-mark.png"; diff --git a/web/public/brand/mark-gradient.svg b/web/public/brand/mark-gradient.svg index e7458e956..412e4c14d 100644 --- a/web/public/brand/mark-gradient.svg +++ b/web/public/brand/mark-gradient.svg @@ -1 +1,9 @@ - + + + + + + + + + diff --git a/web/public/brand/mark-mono.svg b/web/public/brand/mark-mono.svg new file mode 100644 index 000000000..f67e8d277 --- /dev/null +++ b/web/public/brand/mark-mono.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/public/brand/mark-reversed.svg b/web/public/brand/mark-reversed.svg new file mode 100644 index 000000000..90ceae43c --- /dev/null +++ b/web/public/brand/mark-reversed.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/public/brand/mark.svg b/web/public/brand/mark.svg index 75b6a4d86..71d3b534c 100644 --- a/web/public/brand/mark.svg +++ b/web/public/brand/mark.svg @@ -1 +1,3 @@ - + + + diff --git a/web/public/brand/wordmark-inverted.svg b/web/public/brand/wordmark-inverted.svg index e53701417..b0bb87e19 100644 --- a/web/public/brand/wordmark-inverted.svg +++ b/web/public/brand/wordmark-inverted.svg @@ -1 +1,3 @@ - + + + diff --git a/web/public/brand/wordmark.svg b/web/public/brand/wordmark.svg index 81133ab7e..03d61e895 100644 --- a/web/public/brand/wordmark.svg +++ b/web/public/brand/wordmark.svg @@ -1 +1,3 @@ - + + +