Compare commits

...
3 Commits
Author SHA1 Message Date
jknapp fa82d54afa Merge pull request #20: New app icon: a mark that survives being 16 pixels tall
Build App / compute-version (push) Successful in 5s
Build App / build-macos (push) Successful in 2m46s
Build App / build-linux (push) Successful in 5m22s
Build App / build-windows (push) Successful in 5m28s
Build App / create-tag (push) Successful in 3s
Build App / sync-to-github (push) Successful in 12s
2026-08-11 19:04:49 +00:00
shadow-testandClaude Opus 5 4c962ebd9c Archive the marks the new icon replaces
Build App (Preview) / compute-version (pull_request) Successful in 3s
Build App (Preview) / create-release (pull_request) Successful in 1s
Build App (Preview) / build-macos (pull_request) Successful in 2m40s
Build App (Preview) / build-windows (pull_request) Successful in 5m29s
Build App (Preview) / build-linux (pull_request) Successful in 5m41s
Build App (Preview) / prune-previews (pull_request) Successful in 1s
Neither is referenced by the app or the build; branding/archive/README.md
says what each one was and why it did not survive the sizes an app icon
is actually drawn at.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 12:03:28 -07:00
shadow-test d15faa923b Give the app a mark that survives being 16 pixels tall
The icon is now a container with its right wall opened, so the enclosure
itself is the letter C, holding a >_ prompt: the two things the app is,
in one closed shape. It carries no type, so nothing goes illegible when
the shell draws it small, and it uses the app's own accent tokens rather
than a saturated orange field that fights the chrome behind it.

icon.ico contained a single 16x16 image, which Windows was upscaling into
the taskbar and every other slot — the likely cause of the artefact in
screenshot_for_fix/. It now carries 16, 24, 32, 48, 64, 128 and 256, each
rendered from vector rather than downsampled from one bitmap, and the
entries at 32 and below come from a separate optical source: at that size
the cursor bar closes up against the chevron, so the small variant drops
it, widens the mouth and thickens the strokes. A test asserts the .ico
keeps its small sizes so this cannot regress silently.

Also adds the icon.icns that macOS bundles have been building without,
points the favicon at our own mark instead of the missing /vite.svg, and
puts the SVG sources, the lockups and the regeneration script in
branding/.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 12:01:39 -07:00
23 changed files with 314 additions and 1 deletions
+5
View File
@@ -1,3 +1,8 @@
<picture>
<source media="(prefers-color-scheme: dark)" srcset="branding/triple-c-lockup-dark.svg">
<img src="branding/triple-c-lockup-light.svg" alt="Triple-C — Coding Container" width="429" height="112">
</picture>
# Triple-C (Claude-Code-Container)
Triple-C is a cross-platform desktop application that sandboxes Claude Code inside Docker containers. Each project chooses its own **permission mode** — from Plan (read-only) through to Bypass (`--dangerously-skip-permissions`), which gives Claude unrestricted access within the sandbox.
+1 -1
View File
@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Triple-C</title>
</head>
+13
View File
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128" role="img" aria-label="Triple-C">
<title>Triple-C application icon, small-size variant</title>
<!-- Source for every raster ≤ 32 px: the small mark, drawn at 82% so the strokes
survive being resampled down to 16 px. See build-icons.py. -->
<rect width="128" height="128" rx="28.16" fill="#0D1117"/>
<g transform="translate(2.9236 2.9236) scale(0.95418)">
<g fill="none" stroke-linecap="round" stroke-linejoin="round">
<path d="M112 50 L112 38 A22 22 0 0 0 90 16 L38 16 A22 22 0 0 0 16 38 L16 90 A22 22 0 0 0 38 112 L90 112 A22 22 0 0 0 112 90 L112 78"
stroke="#58A6FF" stroke-width="14"/>
<path d="M46 50 L62 66 L46 82" stroke="#F0821E" stroke-width="13"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 812 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 918 B

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 16 KiB

+1
View File
@@ -33,6 +33,7 @@
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.ico",
"icons/icon.icns",
"icons/icon.png"
]
},
+29
View File
@@ -33,4 +33,33 @@ describe("Window icon configuration", () => {
expect(config.bundle.icon).toContain("icons/icon.ico");
expect(config.bundle.icon).toContain("icons/icon.png");
});
it("icon.ico carries the small sizes Windows draws in the taskbar", () => {
// A single-image .ico is the taskbar bug: Windows upscales 16x16 into every
// other slot. Regenerate with `python3 branding/build-icons.py`.
const ico = readFileSync(resolve(srcTauriDir, "icons/icon.ico"));
const count = ico.readUInt16LE(4);
expect(count).toBeGreaterThan(1);
// Directory entries start at byte 6; width/height of 0 means 256.
const widths = new Set<number>();
for (let i = 0; i < count; i++) {
const w = ico[6 + i * 16];
widths.add(w === 0 ? 256 : w);
}
for (const size of [16, 24, 32, 48, 256]) {
expect(widths).toContain(size);
}
});
it("icon.icns exists and is bundled for macOS", () => {
const icnsPath = resolve(srcTauriDir, "icons/icon.icns");
expect(existsSync(icnsPath)).toBe(true);
expect(readFileSync(icnsPath).subarray(0, 4).toString("ascii")).toBe("icns");
const config = JSON.parse(
readFileSync(resolve(srcTauriDir, "tauri.conf.json"), "utf-8")
);
expect(config.bundle.icon).toContain("icons/icon.icns");
});
});
+58
View File
@@ -0,0 +1,58 @@
# Branding
The mark is a container with its right wall opened, so the enclosure itself is the letter **C**,
holding a `>_` prompt. Container plus shell, in one closed shape — no type inside the icon, so
nothing goes illegible when the app is 16 px tall in a taskbar.
## Files
| File | What it is |
|---|---|
| `triple-c-mark.svg` | The mark alone, transparent. Use on any ground. |
| `triple-c-mark-small.svg` | Optical variant for 32 px and below. |
| `triple-c-icon.svg` | The app icon: mark at 70% on a `#0D1117` tile, 22% corner radius. |
| `triple-c-icon-small.svg` | The app icon at small sizes — small mark, drawn at 82%. |
| `triple-c-lockup-dark.svg` | Horizontal lockup for dark backgrounds. Wordmark is outlined, so no font is needed to render it. |
| `triple-c-lockup-light.svg` | The same for light backgrounds. |
| `triple-c-icon-1024.png` | Master raster, generated. |
| `build-icons.py` | Regenerates every packaged icon from the SVGs. |
## Palette
| Role | Dark ground | Light ground |
|---|---|---|
| Frame | `#58A6FF` (`--accent`) | `#1F6FEB` (`--accent-emphasis`) |
| Prompt | `#F0821E` | `#C4610F` |
| Tile | `#0D1117` (`--bg-primary`) | — |
| Wordmark | `#E6EDF3` (`--text-primary`) | `#131C25` |
The frame and prompt colours are the app's own accent tokens from `app/src/index.css`, which is why
the icon sits on the app's chrome instead of fighting it. Orange is the only equity carried over
from the previous marks, and it is now an accent rather than a background.
## Two sources, not one
`build-icons.py` renders sizes ≥ 48 px from `triple-c-icon.svg` and sizes ≤ 32 px from
`triple-c-icon-small.svg`. At small sizes the cursor bar closes up against the chevron and a 12 px
frame stroke resamples to a grey smear, so the small variant drops the cursor, widens the mouth,
thickens the strokes and draws the mark larger inside the tile.
This matters most for `icon.ico`: Windows picks the 16 px entry for the window corner and 24/32 px
for the taskbar. The previous `.ico` contained a *single* 16 px image, which Windows then upscaled
everywhere else — the likely cause of `screenshot_for_fix/task_bar_icon_not_correct.png`. The
current one carries 16, 24, 32, 48, 64, 128 and 256, each rendered from vector rather than
downsampled from one bitmap.
## Regenerating
```bash
pip install cairosvg pillow
python3 branding/build-icons.py
```
Writes `app/src-tauri/icons/{32x32,128x128,128x128@2x,icon}.png`, `icon.ico`, `icon.icns`,
`app/public/favicon.svg` and `branding/triple-c-icon-1024.png`. Do not hand-edit those — edit the
SVG and re-run.
The lockups are committed sources, not generated: their wordmark is Liberation Mono Bold converted
to outlines (`-1.6` tracking, 44 px cap height), so re-cutting it needs the font and `fonttools`.
+11
View File
@@ -0,0 +1,11 @@
# Archive
The marks Triple-C shipped before the current one, kept for reference.
| File | Notes |
|---|---|
| `triple-c-app-logo.png` | First mark: orange tile, overlapping C letterforms, "TRIPLE-C / CONTAINERIZED CODING" set inside the icon. |
| `triple-c-app-logov2.png` | Second mark: orange sunburst with "Triple-C / Coding Container" over it, transparent ground. |
Both were drawn at poster size and carried type inside the icon, so nothing in them survived the
1632 px range where an app icon actually lives. Neither is referenced by the app or the build.
Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Before

Width:  |  Height:  |  Size: 191 KiB

After

Width:  |  Height:  |  Size: 191 KiB

+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env python3
"""Render every packaged icon from the SVG sources in this directory.
pip install cairosvg pillow
python3 branding/build-icons.py
The SVGs are the source of truth; everything under app/src-tauri/icons/ and the
favicon are generated. Two sources, not one, on purpose:
triple-c-icon.svg used for 48 px and up
triple-c-icon-small.svg used for 32 px and down
At 16-32 px the cursor bar closes up against the chevron and the frame's 12 px
stroke resamples to a grey smear, so the small source drops the cursor, widens
the mouth and draws the mark larger in the tile. Windows picks the 16 and 24 px
entries out of the .ico for the taskbar and window corner, which is exactly
where the old single-size .ico was falling over.
"""
import io
import struct
from pathlib import Path
import cairosvg
from PIL import Image
BRANDING = Path(__file__).resolve().parent
REPO = BRANDING.parent
ICONS = REPO / "app" / "src-tauri" / "icons"
PUBLIC = REPO / "app" / "public"
FULL_SRC = BRANDING / "triple-c-icon.svg"
SMALL_SRC = BRANDING / "triple-c-icon-small.svg"
# Below this, render from the small source.
SMALL_MAX = 32
# .ico entries. Windows uses 16 in the window corner and 24/32 in the taskbar,
# 48 in list views and 256 in the "extra large icons" view.
ICO_SIZES = [16, 24, 32, 48, 64, 128, 256]
# .icns entries: (chunk type, pixel size). PNG-backed chunks, macOS 10.7+.
ICNS_ENTRIES = [
(b"ic11", 32), # 16pt @2x
(b"ic12", 64), # 32pt @2x
(b"ic07", 128), # 128pt
(b"ic13", 256), # 128pt @2x
(b"ic08", 256), # 256pt
(b"ic14", 512), # 256pt @2x
(b"ic09", 512), # 512pt
(b"ic10", 1024), # 512pt @2x
]
def render(size: int) -> Image.Image:
"""Rasterise the right source at `size` px square."""
src = SMALL_SRC if size <= SMALL_MAX else FULL_SRC
png = cairosvg.svg2png(url=str(src), output_width=size, output_height=size)
return Image.open(io.BytesIO(png)).convert("RGBA")
def write_png(size: int, path: Path) -> None:
render(size).save(path, "PNG", optimize=True)
print(f" {path.relative_to(REPO)} {size}x{size}")
def write_ico(path: Path) -> None:
"""Hand-assemble the .ico so each entry can come from its own source.
Pillow's save(sizes=...) downsamples one image, which would put the
12 px-stroke artwork into the 16 px entry the thing this avoids.
"""
images = [render(s) for s in ICO_SIZES]
payloads = []
for img in images:
buf = io.BytesIO()
img.save(buf, "PNG", optimize=True) # PNG-compressed entries, Vista+
payloads.append(buf.getvalue())
offset = 6 + 16 * len(images)
header = struct.pack("<HHH", 0, 1, len(images))
entries, blob = b"", b""
for img, data in zip(images, payloads):
w = 0 if img.width >= 256 else img.width
h = 0 if img.height >= 256 else img.height
entries += struct.pack("<BBBBHHII", w, h, 0, 0, 1, 32, len(data), offset)
blob += data
offset += len(data)
path.write_bytes(header + entries + blob)
print(f" {path.relative_to(REPO)} {', '.join(str(s) for s in ICO_SIZES)}")
def write_icns(path: Path) -> None:
chunks = b""
for kind, size in ICNS_ENTRIES:
buf = io.BytesIO()
render(size).save(buf, "PNG", optimize=True)
data = buf.getvalue()
chunks += kind + struct.pack(">I", len(data) + 8) + data
path.write_bytes(b"icns" + struct.pack(">I", len(chunks) + 8) + chunks)
print(f" {path.relative_to(REPO)} {', '.join(str(s) for _, s in ICNS_ENTRIES)}")
def main() -> None:
print("branding/")
write_png(1024, BRANDING / "triple-c-icon-1024.png")
print("app/src-tauri/icons/")
write_png(32, ICONS / "32x32.png")
write_png(128, ICONS / "128x128.png")
write_png(256, ICONS / "128x128@2x.png")
write_png(512, ICONS / "icon.png")
write_ico(ICONS / "icon.ico")
write_icns(ICONS / "icon.icns")
print("app/public/")
(PUBLIC / "favicon.svg").write_text(SMALL_SRC.read_text())
print(f" {(PUBLIC / 'favicon.svg').relative_to(REPO)} (copy of {SMALL_SRC.name})")
if __name__ == "__main__":
main()
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

+13
View File
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128" role="img" aria-label="Triple-C">
<title>Triple-C application icon, small-size variant</title>
<!-- Source for every raster ≤ 32 px: the small mark, drawn at 82% so the strokes
survive being resampled down to 16 px. See build-icons.py. -->
<rect width="128" height="128" rx="28.16" fill="#0D1117"/>
<g transform="translate(2.9236 2.9236) scale(0.95418)">
<g fill="none" stroke-linecap="round" stroke-linejoin="round">
<path d="M112 50 L112 38 A22 22 0 0 0 90 16 L38 16 A22 22 0 0 0 16 38 L16 90 A22 22 0 0 0 38 112 L90 112 A22 22 0 0 0 112 90 L112 78"
stroke="#58A6FF" stroke-width="14"/>
<path d="M46 50 L62 66 L46 82" stroke="#F0821E" stroke-width="13"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 812 B

+14
View File
@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128" role="img" aria-label="Triple-C">
<title>Triple-C application icon</title>
<!-- App icon: the mark on the app's own ground (bg-primary), at 70% of the tile.
Source for every raster ≥ 48 px. See build-icons.py. -->
<rect width="128" height="128" rx="28.16" fill="#0D1117"/>
<g transform="translate(10.8988 10.8988) scale(0.82963)">
<g fill="none" stroke-linecap="round" stroke-linejoin="round">
<path d="M112 46 L112 38 A22 22 0 0 0 90 16 L38 16 A22 22 0 0 0 16 38 L16 90 A22 22 0 0 0 38 112 L90 112 A22 22 0 0 0 112 90 L112 82"
stroke="#58A6FF" stroke-width="12"/>
<path d="M42 50 L58 66 L42 82" stroke="#F0821E" stroke-width="11"/>
<path d="M68 82 L88 82" stroke="#F0821E" stroke-width="11"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 855 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

+10
View File
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128" role="img" aria-label="Triple-C">
<title>Triple-C mark, small-size variant</title>
<!-- Optical variant for 32 px and below: heavier strokes, wider mouth, no cursor bar.
Below ~40 px the cursor closes up against the chevron and the frame goes to mush. -->
<g fill="none" stroke-linecap="round" stroke-linejoin="round">
<path d="M112 50 L112 38 A22 22 0 0 0 90 16 L38 16 A22 22 0 0 0 16 38 L16 90 A22 22 0 0 0 38 112 L90 112 A22 22 0 0 0 112 90 L112 78"
stroke="#58A6FF" stroke-width="14"/>
<path d="M46 50 L62 66 L46 82" stroke="#F0821E" stroke-width="13"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 690 B

+11
View File
@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128" role="img" aria-label="Triple-C">
<title>Triple-C mark</title>
<!-- The container frame, opened on the right so the enclosure itself is the letter C. -->
<g fill="none" stroke-linecap="round" stroke-linejoin="round">
<path d="M112 46 L112 38 A22 22 0 0 0 90 16 L38 16 A22 22 0 0 0 16 38 L16 90 A22 22 0 0 0 38 112 L90 112 A22 22 0 0 0 112 90 L112 82"
stroke="#58A6FF" stroke-width="12"/>
<!-- The prompt: chevron and cursor. -->
<path d="M42 50 L58 66 L42 82" stroke="#F0821E" stroke-width="11"/>
<path d="M68 82 L88 82" stroke="#F0821E" stroke-width="11"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 691 B