feat(apple): styling pass — dark-mode accent, recent-host state, glass HUD, security-sheet polish
ci / rust (push) Has been cancelled
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:
@@ -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). "
|
||||
|
||||
Reference in New Issue
Block a user