Files
punktfunk/clients/apple/Sources/PunktfunkShared/ProfileAccent.swift
T
enricobuehlerandClaude Opus 5 f50d13752d feat(client/apple): an About page worth opening, and profile colours you can actually pick
About was a bare "Punktfunk / Version x" header over 885 KB of license text — it
answered the one question nobody opens About to ask. It is now the shape every Mac
user already knows: the app's icon, its name, the version (selectable, because a bug
report is worth more with it), the tagline, and then the ways out — documentation,
community, source, licenses. The license wall is still `AcknowledgementsView`, one
push in on iOS/tvOS and a sheet on macOS, where a preferences tab has no navigation
stack to push onto.

Every platform hides the app icon somewhere different and tvOS can't hand it over at
all (layered assets, no single image), so there's a drawn fallback in the same brand
gradient and Geist monogram as the host cards — where the real icon is unavailable it
reads as a mark, not as a broken image. tvOS also has no browser: the addresses are
text to read off the screen rather than links to nowhere.

And profile colours were half-built by me: the catalog stored `accent`, the chips
tinted from it, and nothing in the app ever SET one. The scope menu grows "Color ▸"
over a fixed palette — the chip is small tinted text on a tinted capsule, and a
colour picked freehand lands somewhere unreadable often enough to matter. The palette
is what this client OFFERS, not what it accepts: any `#RRGGBB` another platform
writes still renders, which is what Android needs from this field.

The colour now shows wherever the profile is named — the scope switcher's dot, the
card chips, and the HUD's session line, which tints to the same colour the card that
launched it wore. A colour is an identifier, and an identifier that changes between
surfaces isn't one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 12:06:41 +02:00

61 lines
2.7 KiB
Swift

// A profile's chip colour (design/client-settings-profiles.md §5.1 "Change color"). The catalog
// stores `accent` as `#RRGGBB` — a plain string, so the Rust and Kotlin catalogs read the same
// value — and this is the palette the Apple client offers for it.
//
// A fixed palette rather than a full colour well, for two reasons. The chip is small, tinted text
// on a tinted capsule, so a colour picked freehand can land somewhere unreadable; and a profile's
// colour is an IDENTIFIER — "the orange one" — which works when there are eight of them and stops
// working when there are sixteen million. Any `#RRGGBB` a newer client (or another platform)
// writes still renders: the palette is what this client OFFERS, not what it accepts.
import SwiftUI
public struct ProfileAccent: Identifiable, Sendable, Hashable {
/// `#RRGGBB`, lowercase — exactly what lands in the catalog.
public let hex: String
public let name: String
public var id: String { hex }
public var color: Color { Color(hex: hex) ?? .brand }
/// Hues that stay legible as tinted text on their own tinted capsule, in both appearances,
/// and that stay tellable apart from each other at chip size.
public static let palette: [ProfileAccent] = [
ProfileAccent(hex: "#8b5cf6", name: "Violet"),
ProfileAccent(hex: "#3aa0ff", name: "Blue"),
ProfileAccent(hex: "#2bb8a6", name: "Teal"),
ProfileAccent(hex: "#35c759", name: "Green"),
ProfileAccent(hex: "#e0a800", name: "Amber"),
ProfileAccent(hex: "#ff8800", name: "Orange"),
ProfileAccent(hex: "#ff4f6d", name: "Red"),
ProfileAccent(hex: "#ff4f9a", name: "Pink"),
ProfileAccent(hex: "#8e8e93", name: "Graphite"),
]
public static func named(_ hex: String?) -> ProfileAccent? {
guard let hex else { return nil }
return palette.first { $0.hex.caseInsensitiveCompare(hex) == .orderedSame }
}
}
public extension Color {
/// Parse a `#RRGGBB` string. nil for anything else — a profile with an accent this build
/// can't read falls back to the brand tint rather than rendering as black.
init?(hex: String) {
guard hex.hasPrefix("#"), hex.count == 7,
let value = UInt32(hex.dropFirst(), radix: 16)
else { return nil }
self.init(
red: Double((value >> 16) & 0xFF) / 255,
green: Double((value >> 8) & 0xFF) / 255,
blue: Double(value & 0xFF) / 255)
}
}
public extension StreamProfile {
/// This profile's colour, or the brand tint when it has none — every surface that shows a
/// profile reads it through here, so "no colour chosen" looks deliberate everywhere.
var accentColor: Color { Color(hex: accent ?? "") ?? .brand }
}