From ee12e535ee343d65a2611c9a0ce0bad0a405a3c5 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Thu, 11 Jun 2026 12:59:55 +0200 Subject: [PATCH] =?UTF-8?q?feat(apple):=20styling=20pass=20=E2=80=94=20dar?= =?UTF-8?q?k-mode=20accent,=20recent-host=20state,=20glass=20HUD,=20securi?= =?UTF-8?q?ty-sheet=20polish?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../AccentColor.colorset/Contents.json | 18 ++++++++ .../Sources/PunktfunkClient/ContentView.swift | 45 ++++++++++++++++--- .../Sources/PunktfunkClient/HostStore.swift | 7 +++ .../Sources/PunktfunkClient/PairSheet.swift | 8 +++- 4 files changed, 69 insertions(+), 9 deletions(-) diff --git a/clients/apple/App/Assets.xcassets/AccentColor.colorset/Contents.json b/clients/apple/App/Assets.xcassets/AccentColor.colorset/Contents.json index 13e8bf2..5849e7a 100644 --- a/clients/apple/App/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/clients/apple/App/Assets.xcassets/AccentColor.colorset/Contents.json @@ -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" : { diff --git a/clients/apple/Sources/PunktfunkClient/ContentView.swift b/clients/apple/Sources/PunktfunkClient/ContentView.swift index ca1d865..e5731a4 100644 --- a/clients/apple/Sources/PunktfunkClient/ContentView.swift +++ b/clients/apple/Sources/PunktfunkClient/ContentView.swift @@ -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) } diff --git a/clients/apple/Sources/PunktfunkClient/HostStore.swift b/clients/apple/Sources/PunktfunkClient/HostStore.swift index 8d52707..a60319c 100644 --- a/clients/apple/Sources/PunktfunkClient/HostStore.swift +++ b/clients/apple/Sources/PunktfunkClient/HostStore.swift @@ -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 diff --git a/clients/apple/Sources/PunktfunkClient/PairSheet.swift b/clients/apple/Sources/PunktfunkClient/PairSheet.swift index 5e030ac..9d9f5f3 100644 --- a/clients/apple/Sources/PunktfunkClient/PairSheet.swift +++ b/clients/apple/Sources/PunktfunkClient/PairSheet.swift @@ -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). "