Replaces the placeholder coat silhouette with the app's actual subject: Claude's asterisk above a rack of overcoats, on Claude's own coral. The wardrobe is the app in one picture — several coats to pick between, one currently worn. The menu bar stops scaling the app icon down and draws its own glyph instead. At 18pt the icon's three coats, lapels and pockets collapse into a smudge, and its colours ignore the menu bar entirely. The glyph is a template image, so macOS tints it for light and dark and for the open-menu inversion, and re-tints it itself when the theme changes. That also drove its shape: six rays rather than eight, because at that size eight have under a pixel between arms and merge into a blob. The coloured dots beside it don't get that for free — ProfileColor picks a brightness for the current appearance at the moment it's asked, and the title is only rebuilt when profiles start or stop. Observing effectiveAppearance rebuilds it, so a theme switch doesn't leave dots mixed for the old appearance sitting in the menu bar. Everything is still drawn from vector paths rather than exported from a design tool, so each size can be tuned — small sizes need fatter strokes than a straight downscale gives — and nothing binary but the derived assets is committed. The README gains a hero banner, composed from the generated icon so it can't drift out of step with it. About gains the tagline under the icon, names itself in the description rather than opening with a bare verb, and drops the author line.
83 lines
3.6 KiB
Swift
83 lines
3.6 KiB
Swift
// The menu bar's own glyph — Claude's asterisk over a coat's collar and
|
|
// shoulders. Drawn here rather than scaled down from the app icon, which is
|
|
// what the menu bar used to show: at 18pt the app icon's three coats, lapels
|
|
// and pockets collapse into a smudge, and its colours fight the menu bar
|
|
// instead of following it.
|
|
//
|
|
// Returned as a *template* image, which is what makes it track the system
|
|
// theme with no code of ours involved. A template carries only an alpha
|
|
// channel; macOS supplies the colour — dark on a light menu bar, light on a
|
|
// dark one, inverted again while the menu is open — and re-tints it on the fly
|
|
// when the theme switches. That also dictates the drawing: no fills of our
|
|
// own, and any internal detail has to be a hole punched through the alpha.
|
|
import AppKit
|
|
|
|
enum MenuBarIcon {
|
|
// Six rays rather than the app icon's eight. At 18pt an eight-ray asterisk
|
|
// has under a pixel of gap between arms, so they merge into a blob; six
|
|
// resolve as separate arms. Lengths stay slightly uneven, as on the app
|
|
// icon, so it doesn't read as a snowflake.
|
|
private static let rayCount = 6
|
|
private static let rayLengths: [CGFloat] = [1.0, 0.88, 0.97, 0.86, 1.0, 0.9]
|
|
private static let rayRadius: CGFloat = 22
|
|
private static let rayWidth: CGFloat = 7.2
|
|
private static let asteriskCentre = NSPoint(x: 50, y: 28)
|
|
|
|
static func image(pointSize: CGFloat = 18) -> NSImage {
|
|
let image = NSImage(size: NSSize(width: pointSize, height: pointSize))
|
|
image.lockFocusFlipped(false)
|
|
|
|
// Authored y-down like the app icon; flip once rather than mirroring
|
|
// every coordinate.
|
|
let flip = NSAffineTransform()
|
|
flip.translateX(by: 0, yBy: pointSize)
|
|
flip.scaleX(by: pointSize / 100, yBy: -pointSize / 100)
|
|
flip.concat()
|
|
|
|
NSColor.black.setStroke()
|
|
asterisk().stroke()
|
|
NSColor.black.setFill()
|
|
coat().fill()
|
|
|
|
image.unlockFocus()
|
|
image.isTemplate = true
|
|
return image
|
|
}
|
|
|
|
private static func asterisk() -> NSBezierPath {
|
|
let path = NSBezierPath()
|
|
for index in 0..<rayCount {
|
|
let angle = (CGFloat(index) * 360 / CGFloat(rayCount) + 8) * .pi / 180
|
|
let radius = rayRadius * rayLengths[index]
|
|
path.move(to: asteriskCentre)
|
|
path.line(to: NSPoint(x: asteriskCentre.x + radius * cos(angle),
|
|
y: asteriskCentre.y + radius * sin(angle)))
|
|
}
|
|
path.lineWidth = rayWidth
|
|
path.lineCapStyle = .round
|
|
return path
|
|
}
|
|
|
|
// Read along the top edge from one side to the other: shoulder, a slight
|
|
// tilt up, a flat collar top, the valley at the neck, then the mirror of
|
|
// that. Square sides and a flat hem below.
|
|
//
|
|
// The flat sections are 13 units wide because anything narrower stops
|
|
// reading as flat at this size and just looks like part of the slope —
|
|
// 13 units is roughly 2.3px at 18pt, about the floor for that.
|
|
private static func coat() -> NSBezierPath {
|
|
let path = NSBezierPath()
|
|
path.move(to: NSPoint(x: 15, y: 72)) // left shoulder
|
|
path.line(to: NSPoint(x: 27, y: 60)) // slight tilt up
|
|
path.line(to: NSPoint(x: 40, y: 60)) // flat collar top
|
|
path.line(to: NSPoint(x: 50, y: 78)) // valley at the neck
|
|
path.line(to: NSPoint(x: 60, y: 60))
|
|
path.line(to: NSPoint(x: 73, y: 60))
|
|
path.line(to: NSPoint(x: 85, y: 72)) // right shoulder
|
|
path.line(to: NSPoint(x: 85, y: 94))
|
|
path.line(to: NSPoint(x: 15, y: 94))
|
|
path.close()
|
|
return path
|
|
}
|
|
}
|