Brand colours on the marks; round the app's corners; move the lights in

Simple Icons publishes each brand's own hex next to its glyph, and the
build was throwing it away. Tiles now carry it - Gmail red, Drive blue,
Chat green, Gemini violet - and the glyph is drawn black or white by the
tile's perceived brightness, since brand colours are picked to look right
rather than to carry a white mark. GitHub and Notion are near-black,
Snapchat is pure yellow, and a fixed white glyph loses one end of that.

An app's right corners are now rounded on its own layer. The container's
rounded-xl could never have clipped them: an app is a native view sitting
on top, not something the shell lays out, so it kept square corners and
overhung the curve. Only the right pair - the left edge butts against the
nav, and rounding it would notch the middle of the window.

The traffic lights move to (26, 24) and the drag strip deepens to match,
so they sit inside the window's margin instead of against its corner.

Adds Gemini, Claude, ChatGPT, Linear, ClickUp, HubSpot, Shopify and
Cloudflare to the host table.
This commit is contained in:
2026-09-01 14:51:20 +02:00
parent ada18d54ae
commit ffedb72b83
10 changed files with 175 additions and 43 deletions
+7 -2
View File
@@ -23,8 +23,13 @@ interface Props {
*/
const RAIL = 72;
const PANEL = 240;
/** The strip the traffic lights sit in. Draggable, since there is no title bar. */
const TITLEBAR = 36;
/**
* The strip the traffic lights sit in. Draggable, since there is no title bar.
*
* Deep enough to give them room: with the window's own frame around it too,
* anything shallower leaves them crowding the corner.
*/
const TITLEBAR = 48;
export const navWidth = (collapsed: boolean) => (collapsed ? RAIL : PANEL);
+19 -9
View File
@@ -7,7 +7,14 @@
*/
import { useEffect, useState, type ReactNode } from "react";
import { colourFor, hostOf, loadBrandIcons, slugForHost } from "../brandIcons";
import {
colourFor,
glyphOn,
hostOf,
loadBrandIcons,
slugForHost,
type BrandIcon,
} from "../brandIcons";
/** Every control in the app is this tall, so a row of mixed ones lines up. */
export const CONTROL_H = "h-[30px]";
@@ -218,7 +225,7 @@ export function Favicon({
name: string;
size?: number;
}) {
const [icons, setIcons] = useState<Record<string, string> | null>(null);
const [icons, setIcons] = useState<Record<string, BrandIcon> | null>(null);
useEffect(() => {
let live = true;
@@ -230,8 +237,11 @@ export function Favicon({
const host = hostOf(url);
const slug = icons ? slugForHost(host, icons) : null;
const path = slug ? icons?.[slug] : undefined;
const colour = colourFor(host || name);
const icon = slug ? icons?.[slug] : undefined;
// The brand's own colour where there is one; a stable stand-in where there
// is not, so an unbranded tool still reads as a distinct thing in the list.
const colour = icon ? `#${icon[1]}` : colourFor(host || name);
const ink = glyphOn(colour);
const letter = name.trim().charAt(0).toUpperCase() || "?";
return (
@@ -240,20 +250,20 @@ export function Favicon({
style={{ width: size, height: size, background: colour }}
aria-hidden
>
{path ? (
{icon ? (
<svg
viewBox="0 0 24 24"
fill="#fff"
fill={ink}
style={{ width: size * 0.58, height: size * 0.58 }}
>
<path d={path} />
<path d={icon[0]} />
</svg>
) : (
/* No glyph for this host — its initial, in the same round tile, so a
private tool sits in the row looking like it belongs. */
<span
className="font-semibold leading-none text-white"
style={{ fontSize: Math.max(8, size * 0.5) }}
className="font-semibold leading-none"
style={{ fontSize: Math.max(8, size * 0.5), color: ink }}
>
{letter}
</span>