New app icon: a mark that survives being 16 pixels tall #20
@@ -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 (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.
|
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.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<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" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Triple-C</title>
|
<title>Triple-C</title>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -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 |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 918 B After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 16 KiB |
@@ -33,6 +33,7 @@
|
|||||||
"icons/128x128.png",
|
"icons/128x128.png",
|
||||||
"icons/128x128@2x.png",
|
"icons/128x128@2x.png",
|
||||||
"icons/icon.ico",
|
"icons/icon.ico",
|
||||||
|
"icons/icon.icns",
|
||||||
"icons/icon.png"
|
"icons/icon.png"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -33,4 +33,33 @@ describe("Window icon configuration", () => {
|
|||||||
expect(config.bundle.icon).toContain("icons/icon.ico");
|
expect(config.bundle.icon).toContain("icons/icon.ico");
|
||||||
expect(config.bundle.icon).toContain("icons/icon.png");
|
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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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`.
|
||||||
@@ -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()
|
||||||
|
After Width: | Height: | Size: 36 KiB |
@@ -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 |
@@ -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 |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -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 |
@@ -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 |