feat(apple): styling pass — dark-mode accent, recent-host state, glass HUD, security-sheet polish
ci / rust (push) Has been cancelled

Working through the brand-color follow-ups:

- AccentColor gains a dark-appearance variant (#8678F5 — the brand violet lifted one
  step toward the icon's light periwinkle) so tinted controls keep contrast on dark.
- Host cards remember sessions: StoredHost.lastConnected (set when a session reaches
  streaming) renders as a "Connected … ago" relative-time line, and the most recent
  host's card carries a subtle accent ring — the grid finally has hierarchy.
- The HUD swaps the pre-glass black-50% rectangle for .regularMaterial with an accent
  live-dot; hint lines use semantic .secondary instead of opacity.
- Security moments: the trust card's lock.shield and the pairing sheet's header take
  the brand tint; the PIN field is larger monospaced and uses the number pad on iOS.

Icon ↔ accent decision: the accent stays the exact brand #6656F2; the Icon Composer
layers keep their adjacent palette (#6C5BF3 family) — close enough to read as one
brand, and the icon remains the design-tool source of truth.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 12:59:55 +02:00
parent b7a6670b4a
commit ee12e535ee
4 changed files with 69 additions and 9 deletions
@@ -11,6 +11,24 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xF5",
"green" : "0x78",
"red" : "0x86"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -44,6 +44,13 @@ struct ContentView: View {
seedDefaultModeIfNeeded()
autoConnectIfAsked()
}
.onChange(of: model.phase) { _, phase in
// A session actually started remember it on the card ("Connected ago"
// plus the accent ring on the most recent host).
if case .streaming = phase, let host = model.activeHost {
store.markConnected(host.id)
}
}
.onDisappear { model.disconnect() } // window closed mid-session (Cmd+N spawns more)
// On the outer Group so the sheet survives the trust-prompt home transition
// (the "Pair with PIN instead" path disconnects first the host's accept loop
@@ -244,12 +251,24 @@ struct ContentView: View {
.foregroundStyle(.secondary)
.lineLimit(1)
}
if let last = host.lastConnected {
Text("Connected \(last, format: .relative(presentation: .named))")
.font(.caption2)
.foregroundStyle(.tertiary)
.lineLimit(1)
}
}
}
.frame(maxWidth: .infinity)
.padding(.vertical, cardPadding)
.padding(.horizontal, 12)
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 14))
.overlay {
if host.id == mostRecentHostID {
RoundedRectangle(cornerRadius: 14)
.strokeBorder(Color.accentColor.opacity(0.35), lineWidth: 1.5)
}
}
}
.buttonStyle(.plain)
.disabled(model.isBusy)
@@ -280,6 +299,13 @@ struct ContentView: View {
#endif
}
/// The host of the most recent session its card carries the accent ring.
private var mostRecentHostID: UUID? {
store.hosts
.compactMap { host in host.lastConnected.map { (host.id, $0) } }
.max { $0.1 < $1.1 }?.0
}
private func connect(_ host: StoredHost) {
model.connect(
to: host,
@@ -295,6 +321,7 @@ struct ContentView: View {
VStack(spacing: 14) {
Image(systemName: "lock.shield")
.font(.system(size: 36, weight: .light))
.foregroundStyle(.tint)
Text("Verify \(model.activeHost?.displayName ?? "host")")
.font(.title3.weight(.semibold))
Text("First connection. Compare this fingerprint with the one "
@@ -377,8 +404,13 @@ struct ContentView: View {
private func hud(_ conn: PunktfunkConnection) -> some View {
VStack(alignment: .trailing, spacing: 4) {
Text("\(conn.width)×\(conn.height)@\(conn.refreshHz) \(model.fps) fps \(model.mbps, specifier: "%.1f") Mb/s")
.font(.system(.caption, design: .monospaced))
HStack(spacing: 6) {
Circle()
.fill(Color.accentColor)
.frame(width: 7, height: 7)
Text("\(conn.width)×\(conn.height)@\(conn.refreshHz) \(model.fps) fps \(model.mbps, specifier: "%.1f") Mb/s")
.font(.system(.caption, design: .monospaced))
}
// While captured the cursor is hidden+frozen, so the button is keyboard-only
// ( or Cmd+Tab release the cursor; released, it's clickable again).
#if os(macOS)
@@ -386,22 +418,21 @@ struct ContentView: View {
? "⌘⎋ releases the mouse"
: "Click the stream to capture input")
.font(.caption2)
.opacity(0.8)
.foregroundStyle(.secondary)
#else
// Touch always plays directly; (hardware keyboard) toggles kb/mouse.
Text(model.mouseCaptured
? "⌘⎋ releases keyboard & mouse"
: "⌘⎋ captures keyboard & mouse")
.font(.caption2)
.opacity(0.8)
.foregroundStyle(.secondary)
#endif
Button("Disconnect (⌘D)") { model.disconnect() }
.font(.caption)
.keyboardShortcut("d", modifiers: .command)
}
.padding(8)
.background(.black.opacity(0.5), in: RoundedRectangle(cornerRadius: 6))
.foregroundStyle(.white)
.padding(10)
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 10))
.padding(10)
}
@@ -18,6 +18,8 @@ struct StoredHost: Identifiable, Codable, Hashable {
var port: UInt16 = 9777
/// SHA-256 of the host's certificate, set after the user explicitly trusted it.
var pinnedSHA256: Data?
/// Last time a streaming session actually started (nil until the first one).
var lastConnected: Date?
var displayName: String { name.isEmpty ? address : name }
}
@@ -47,6 +49,11 @@ final class HostStore: ObservableObject {
hosts.removeAll { $0.id == host.id }
}
func markConnected(_ hostID: UUID) {
guard let i = hosts.firstIndex(where: { $0.id == hostID }) else { return }
hosts[i].lastConnected = Date()
}
func pin(_ hostID: UUID, fingerprint: Data) {
guard let i = hosts.firstIndex(where: { $0.id == hostID }) else { return }
hosts[i].pinnedSHA256 = fingerprint
@@ -39,12 +39,16 @@ struct PairSheet: View {
Form {
Section {
TextField("PIN", text: $pin, prompt: Text("Shown in the host's log"))
.font(.system(.body, design: .monospaced))
.font(.system(.title3, design: .monospaced))
#if os(iOS)
.keyboardType(.numberPad)
#endif
TextField(
"Client name", text: $clientName,
prompt: Text("How the host lists this Mac"))
} header: {
Text("Pair with \(host.displayName)")
Label("Pair with \(host.displayName)", systemImage: "lock.shield")
.foregroundStyle(.tint)
} footer: {
Text("The host prints the PIN when pairing is armed "
+ "(--allow-pairing, \u{201C}PAIRING ARMED\u{201D} in its log). "