The create/update flow was four menu items, three bare text alerts and a submenu of colour NAMES with no colour anywhere on it. Making a "Work" profile meant: name it in an alert, find it again in the scope menu, open a submenu, and pick "Amber" on faith. A profile is a name and a colour, so they are decided together now — in one editor sheet that serves create, duplicate and edit, with a live chip at the top showing exactly what the host cards will render. The palette is swatches you can see, in a grid, with a checkmark AND a ring on the chosen one (the tick alone washes out on the pale hues) and a 44pt target under each 30pt dot. Default leads the row, so "no colour" is a choice on the same shelf as the rest rather than the absence of one. Duplicate opens the sheet rather than committing on the spot: it arrives carrying the source's colour and overrides with a free name filled in, so it can be renamed before it exists rather than after. Creating lands you in the new profile — being left on the defaults is how you end up editing the wrong layer. Delete moved into the sheet with the rest, and its warning is where the button is: the count of hosts and pinned cards that will change sits under it before you press it, not only in the confirmation after. The name's uniqueness check now reports inline and in red instead of through a disabled button and an alert message. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
62 lines
2.8 KiB
Swift
62 lines
2.8 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.
|
|
/// No violet: the brand purple IS the default, and a swatch a shade off it would be a
|
|
/// choice you can't see you made.
|
|
public static let palette: [ProfileAccent] = [
|
|
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 }
|
|
}
|