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"
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0xF5",
|
||||||
|
"green" : "0x78",
|
||||||
|
"red" : "0x86"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@@ -44,6 +44,13 @@ struct ContentView: View {
|
|||||||
seedDefaultModeIfNeeded()
|
seedDefaultModeIfNeeded()
|
||||||
autoConnectIfAsked()
|
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)
|
.onDisappear { model.disconnect() } // window closed mid-session (Cmd+N spawns more)
|
||||||
// On the outer Group so the sheet survives the trust-prompt → home transition
|
// 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
|
// (the "Pair with PIN instead" path disconnects first — the host's accept loop
|
||||||
@@ -244,12 +251,24 @@ struct ContentView: View {
|
|||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
}
|
}
|
||||||
|
if let last = host.lastConnected {
|
||||||
|
Text("Connected \(last, format: .relative(presentation: .named))")
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.tertiary)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.padding(.vertical, cardPadding)
|
.padding(.vertical, cardPadding)
|
||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 12)
|
||||||
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 14))
|
.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)
|
.buttonStyle(.plain)
|
||||||
.disabled(model.isBusy)
|
.disabled(model.isBusy)
|
||||||
@@ -280,6 +299,13 @@ struct ContentView: View {
|
|||||||
#endif
|
#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) {
|
private func connect(_ host: StoredHost) {
|
||||||
model.connect(
|
model.connect(
|
||||||
to: host,
|
to: host,
|
||||||
@@ -295,6 +321,7 @@ struct ContentView: View {
|
|||||||
VStack(spacing: 14) {
|
VStack(spacing: 14) {
|
||||||
Image(systemName: "lock.shield")
|
Image(systemName: "lock.shield")
|
||||||
.font(.system(size: 36, weight: .light))
|
.font(.system(size: 36, weight: .light))
|
||||||
|
.foregroundStyle(.tint)
|
||||||
Text("Verify \(model.activeHost?.displayName ?? "host")")
|
Text("Verify \(model.activeHost?.displayName ?? "host")")
|
||||||
.font(.title3.weight(.semibold))
|
.font(.title3.weight(.semibold))
|
||||||
Text("First connection. Compare this fingerprint with the one "
|
Text("First connection. Compare this fingerprint with the one "
|
||||||
@@ -377,8 +404,13 @@ struct ContentView: View {
|
|||||||
|
|
||||||
private func hud(_ conn: PunktfunkConnection) -> some View {
|
private func hud(_ conn: PunktfunkConnection) -> some View {
|
||||||
VStack(alignment: .trailing, spacing: 4) {
|
VStack(alignment: .trailing, spacing: 4) {
|
||||||
|
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")
|
Text("\(conn.width)×\(conn.height)@\(conn.refreshHz) \(model.fps) fps \(model.mbps, specifier: "%.1f") Mb/s")
|
||||||
.font(.system(.caption, design: .monospaced))
|
.font(.system(.caption, design: .monospaced))
|
||||||
|
}
|
||||||
// While captured the cursor is hidden+frozen, so the button is keyboard-only
|
// While captured the cursor is hidden+frozen, so the button is keyboard-only
|
||||||
// (⌘⎋ or Cmd+Tab release the cursor; released, it's clickable again).
|
// (⌘⎋ or Cmd+Tab release the cursor; released, it's clickable again).
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
@@ -386,22 +418,21 @@ struct ContentView: View {
|
|||||||
? "⌘⎋ releases the mouse"
|
? "⌘⎋ releases the mouse"
|
||||||
: "Click the stream to capture input")
|
: "Click the stream to capture input")
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
.opacity(0.8)
|
.foregroundStyle(.secondary)
|
||||||
#else
|
#else
|
||||||
// Touch always plays directly; ⌘⎋ (hardware keyboard) toggles kb/mouse.
|
// Touch always plays directly; ⌘⎋ (hardware keyboard) toggles kb/mouse.
|
||||||
Text(model.mouseCaptured
|
Text(model.mouseCaptured
|
||||||
? "⌘⎋ releases keyboard & mouse"
|
? "⌘⎋ releases keyboard & mouse"
|
||||||
: "⌘⎋ captures keyboard & mouse")
|
: "⌘⎋ captures keyboard & mouse")
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
.opacity(0.8)
|
.foregroundStyle(.secondary)
|
||||||
#endif
|
#endif
|
||||||
Button("Disconnect (⌘D)") { model.disconnect() }
|
Button("Disconnect (⌘D)") { model.disconnect() }
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.keyboardShortcut("d", modifiers: .command)
|
.keyboardShortcut("d", modifiers: .command)
|
||||||
}
|
}
|
||||||
.padding(8)
|
.padding(10)
|
||||||
.background(.black.opacity(0.5), in: RoundedRectangle(cornerRadius: 6))
|
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 10))
|
||||||
.foregroundStyle(.white)
|
|
||||||
.padding(10)
|
.padding(10)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ struct StoredHost: Identifiable, Codable, Hashable {
|
|||||||
var port: UInt16 = 9777
|
var port: UInt16 = 9777
|
||||||
/// SHA-256 of the host's certificate, set after the user explicitly trusted it.
|
/// SHA-256 of the host's certificate, set after the user explicitly trusted it.
|
||||||
var pinnedSHA256: Data?
|
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 }
|
var displayName: String { name.isEmpty ? address : name }
|
||||||
}
|
}
|
||||||
@@ -47,6 +49,11 @@ final class HostStore: ObservableObject {
|
|||||||
hosts.removeAll { $0.id == host.id }
|
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) {
|
func pin(_ hostID: UUID, fingerprint: Data) {
|
||||||
guard let i = hosts.firstIndex(where: { $0.id == hostID }) else { return }
|
guard let i = hosts.firstIndex(where: { $0.id == hostID }) else { return }
|
||||||
hosts[i].pinnedSHA256 = fingerprint
|
hosts[i].pinnedSHA256 = fingerprint
|
||||||
|
|||||||
@@ -39,12 +39,16 @@ struct PairSheet: View {
|
|||||||
Form {
|
Form {
|
||||||
Section {
|
Section {
|
||||||
TextField("PIN", text: $pin, prompt: Text("Shown in the host's log"))
|
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(
|
TextField(
|
||||||
"Client name", text: $clientName,
|
"Client name", text: $clientName,
|
||||||
prompt: Text("How the host lists this Mac"))
|
prompt: Text("How the host lists this Mac"))
|
||||||
} header: {
|
} header: {
|
||||||
Text("Pair with \(host.displayName)")
|
Label("Pair with \(host.displayName)", systemImage: "lock.shield")
|
||||||
|
.foregroundStyle(.tint)
|
||||||
} footer: {
|
} footer: {
|
||||||
Text("The host prints the PIN when pairing is armed "
|
Text("The host prints the PIN when pairing is armed "
|
||||||
+ "(--allow-pairing, \u{201C}PAIRING ARMED\u{201D} in its log). "
|
+ "(--allow-pairing, \u{201C}PAIRING ARMED\u{201D} in its log). "
|
||||||
|
|||||||
Reference in New Issue
Block a user