fix(client/apple): three things the first on-glass run found

Reset read as prose. Next to "Overrides Default settings" in the same tint and the
same size, the one action that can undo an override looked like the third word of
the notice. It is a bordered control now, pushed to the far edge of the caption
line with an undo glyph.

The scope switcher showed above the About tab. It sat over the whole macOS TabView,
so on the acknowledgements page — which edits nothing — it read as belonging to
them. The tabs are tagged and the switcher sits that one out.

Pinned cards were taller than their host's. The profile chip had a line of its own,
so a card with a profile and one without were different heights and a pin stuck out
of its grid row. The chip rides the title line instead, which is also where it reads
as "this card connects with that". Prominence is fill and weight only — a chip with
a bigger type size would have brought the height difference straight back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-29 12:06:41 +02:00
co-authored by Claude Opus 5
parent 0a32f4eab1
commit 82f5962bec
4 changed files with 70 additions and 21 deletions
@@ -42,6 +42,9 @@ private struct HomeTile: Identifiable {
/// The profile this tile connects with shown as a tinted chip. Set on a pinned card; nil on
/// a plain host tile unless the host is bound to one (then it answers "what will A do?").
var profile: StreamProfile?
/// This tile IS a pinned host+profile card, not a host wearing its binding's chip the chip
/// reads loud on the former (it is the reason the tile exists) and quiet on the latter.
var isPinnedCard = false
var isOnline = false
var isPaired = false
var isConnecting = false
@@ -273,6 +276,7 @@ struct GamepadHomeView: View {
title: host.displayName,
subtitle: "\(host.address):\(String(host.port))",
profile: profile ?? bound,
isPinnedCard: profile != nil,
isOnline: online,
isPaired: host.pinnedSHA256 != nil,
isConnecting: connecting,
@@ -374,7 +378,8 @@ private struct GamepadHostTile: View {
.lineLimit(1)
.minimumScaleFactor(0.7)
if let profile = tile.profile {
ProfileChip(profile: profile, size: Self.statusFont, prominent: true)
ProfileChip(
profile: profile, size: Self.statusFont, prominent: tile.isPinnedCard)
.padding(.top, 4)
}
Text(tile.subtitle)
@@ -135,12 +135,23 @@ struct HostCardView: View {
HStack(spacing: m.spacing) {
monogramTile(monogram(host.displayName), m: m, connecting: isConnecting, filled: true)
VStack(alignment: .leading, spacing: 4) {
Text(host.displayName)
.font(.geist(m.name, .bold, relativeTo: .title3))
.foregroundStyle(.primary)
.lineLimit(1)
if let profile = shownProfile {
ProfileChip(profile: profile, size: m.status, prominent: pinnedProfile != nil)
// The chip rides the TITLE line rather than a line of its own: a card with a
// profile and one without have to be the same height, or a pinned card sticks
// out of its row in the grid. Beside the name is also where it reads as "this
// card connects with that", which is what it means.
HStack(spacing: 6) {
Text(host.displayName)
.font(.geist(m.name, .bold, relativeTo: .title3))
.foregroundStyle(.primary)
.lineLimit(1)
if let profile = shownProfile {
ProfileChip(
profile: profile, size: m.status,
prominent: pinnedProfile != nil)
// The chip is the shorter, more compressible of the two; let the
// host name give up its width first only after the chip has.
.layoutPriority(1)
}
}
Text("\(host.address):\(String(host.port))")
.font(.geist(m.meta, relativeTo: .caption))
@@ -301,6 +312,9 @@ struct HostCardView: View {
/// The profile a card connects with, as a tinted pill. Quiet on a bound primary card (it only
/// answers "what will a click do?"); prominent on a pinned card, where the profile IS the reason
/// the card exists which is where the catalog's `accent` earns its keep.
///
/// Prominence is fill and weight only, never TYPE SIZE: the chip sits on the card's title line,
/// and a chip taller than the name would make pinned cards taller than their host's.
struct ProfileChip: View {
let profile: StreamProfile
let size: CGFloat
@@ -314,13 +328,13 @@ struct ProfileChip: View {
.frame(width: size * 0.55, height: size * 0.55)
.accessibilityHidden(true) // the name is right there
Text(profile.name)
.font(.geist(prominent ? size + 2 : size, .semibold, relativeTo: .caption2))
.font(.geist(size, prominent ? .bold : .semibold, relativeTo: .caption2))
.lineLimit(1)
}
.foregroundStyle(tint)
.padding(.horizontal, 7)
.padding(.vertical, 2)
.background(Capsule().fill(tint.opacity(prominent ? 0.22 : 0.13)))
.background(Capsule().fill(tint.opacity(prominent ? 0.24 : 0.12)))
.accessibilityLabel("Profile \(profile.name)")
}
}
@@ -244,9 +244,13 @@ extension SettingsView {
profiles.clearOverride(id, field: name)
}
/// The accent dot + "Reset" affordance a row carries while it overrides the defaults. Rendered
/// The accent dot + Reset affordance a row carries while it overrides the defaults. Rendered
/// inside `described`'s caption line, so it sits with the row it belongs to instead of in some
/// separate list of exceptions.
///
/// Reset is a BORDERED control pushed to the far edge, not tinted text next to the label: as a
/// plain button it read as a third word of the notice, and the one action that can undo an
/// override must not look like prose.
@ViewBuilder
func overrideMarker(_ name: String) -> some View {
if isOverridden(name) {
@@ -259,12 +263,19 @@ extension SettingsView {
Text("Overrides Default settings")
.font(.geist(12, .medium, relativeTo: .caption2))
.foregroundStyle(Color.brand)
Button("Reset") { resetOverride(name) }
.buttonStyle(.plain)
.font(.geist(12, .medium, relativeTo: .caption2))
.foregroundStyle(.tint)
.accessibilityLabel("Reset to Default settings")
Spacer(minLength: 12)
Button {
resetOverride(name)
} label: {
Label("Reset", systemImage: "arrow.uturn.backward")
.font(.geist(11, .medium, relativeTo: .caption2))
}
.buttonStyle(.bordered)
.controlSize(.small)
.accessibilityLabel("Reset to Default settings")
.help("Stop overriding this — follow Default settings again")
}
.frame(maxWidth: 360, alignment: .leading) // the described-row caption's line cap
}
}
@@ -29,6 +29,9 @@ struct SettingsView: View {
@State var nameAction: ProfileNameAction?
@State var nameDraft = ""
@State var profilePendingDelete: StreamProfile?
#if os(macOS)
@State private var macTab: MacTab = .general
#endif
@AppStorage(DefaultsKey.streamWidth) var width = 1920
@AppStorage(DefaultsKey.streamHeight) var height = 1080
@AppStorage(DefaultsKey.streamHz) var hz = 60
@@ -133,14 +136,24 @@ struct SettingsView: View {
// MARK: - macOS: tabbed preferences
#if os(macOS)
/// The preferences tabs, tagged so the scope control can sit out the one page that isn't a
/// settings layer at all.
private enum MacTab: Hashable {
case general, display, input, audio, controllers, about
}
private var macBody: some View {
// The scope control heads the window above the tabs, because it is about which layer
// every tab is editing, not about any one of them.
// every tab is editing, not about any one of them. About is the exception: it edits
// nothing, and directly above the acknowledgements the switcher read as belonging to
// them.
VStack(alignment: .leading, spacing: 0) {
scopeSwitcher
.padding(.horizontal, 20)
.padding(.top, 14)
.padding(.bottom, 10)
if macTab != .about {
scopeSwitcher
.padding(.horizontal, 20)
.padding(.top, 14)
.padding(.bottom, 10)
}
macTabs
}
.frame(width: 500, height: 580)
@@ -149,7 +162,7 @@ struct SettingsView: View {
private var macTabs: some View {
// Tab map mirrors SettingsCategory: General = session/app behavior, Display = the whole
// picture (resolution lives here), Input = keyboard & mouse.
TabView {
TabView(selection: $macTab) {
Form {
sessionSection
overlaySection
@@ -157,6 +170,7 @@ struct SettingsView: View {
}
.formStyle(.grouped)
.tabItem { Label("General", systemImage: "gearshape") }
.tag(MacTab.general)
Form {
resolutionSection
@@ -166,12 +180,14 @@ struct SettingsView: View {
}
.formStyle(.grouped)
.tabItem { Label("Display", systemImage: "display") }
.tag(MacTab.display)
Form {
inputSection
}
.formStyle(.grouped)
.tabItem { Label("Input", systemImage: "keyboard") }
.tag(MacTab.input)
Form {
audioSection
@@ -188,6 +204,7 @@ struct SettingsView: View {
if micChannel > micChannelCount { micChannel = 0 }
}
.tabItem { Label("Audio", systemImage: "speaker.wave.2") }
.tag(MacTab.audio)
Form {
controllersSection
@@ -199,9 +216,11 @@ struct SettingsView: View {
}
.onDisappear { gamepads.stopDiscovery() }
.tabItem { Label("Controllers", systemImage: "gamecontroller") }
.tag(MacTab.controllers)
AcknowledgementsView()
.tabItem { Label("About", systemImage: "info.circle") }
.tag(MacTab.about)
}
}
#endif