feat(client/apple): the host tile wears its OS mark where the initial was
apple / swift (push) Successful in 4m32s
ci / web (push) Successful in 1m5s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 7s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 6s
ci / docs-site (push) Successful in 1m18s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 8s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 7s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 18s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 21s
docker / builders-arm64cross (push) Successful in 13s
docker / deploy-docs (push) Successful in 40s
ci / rust (push) Failing after 6m10s
ci / rust-arm64 (push) Successful in 14m47s
release / apple (push) Successful in 25m17s
apple / screenshots (push) Canceled after 13m52s

Apple parity with the Android card (a94b1d3c's sibling): the OS mark moves
out of the status row and into the tile, replacing the monogram. The
initial says nothing the name beside it doesn't already say — twice over on
a row of home-worker-N boxes — while the mark identifies the machine at a
glance.

Both host surfaces, because on tvOS the console home is the only one there
is: the touch cards (saved and discovered) and GamepadHomeView's badge,
which needed the chain carried on HomeTile to reach it. Sized to the
monogram's own point size, so it lands at ~48% of the tile everywhere, and
tinted through the same foregroundStyle the letter used — the assets are
template imagesets, so they follow it like an SF Symbol.

A host that advertises no OS chain, or one we ship no art for, keeps its
letter: `osIconImage` already returns nil for both, so a mixed row still
reads as one set. The mark carries the accessibility label now, since the
status row it used to ride no longer names the OS.

Untouched: the widget draws no monogram, and AboutView's is the app's own
logo, not a host's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-30 00:53:46 +02:00
co-authored by Claude Opus 5
parent af817bc03e
commit 31db452ca9
2 changed files with 40 additions and 23 deletions
@@ -54,6 +54,9 @@ private struct HomeTile: Identifiable {
var hasLibrary = false
/// Shows this SF symbol in the badge instead of the title monogram (the Add Host tile).
var icon: String?
/// The host's OS-identity chain. When we ship art for it, the badge wears the OS mark instead
/// of the title's initial the same substitution the touch cards make.
var osChain: String?
/// Offline saved host we hold a MAC for (and WoL is available) activating it wakes first.
var canWake = false
let activate: () -> Void
@@ -284,6 +287,7 @@ struct GamepadHomeView: View {
// A pinned card is a shortcut, not a second host Y (library) stays on the
// host's own tile, where the host-level actions live.
hasLibrary: profile == nil,
osChain: host.osChain,
canWake: autoWakeEnabled && PunktfunkConnection.wakeOnLANAvailable
&& !online && !host.wakeMacs.isEmpty,
activate: {
@@ -297,6 +301,7 @@ struct GamepadHomeView: View {
title: d.name,
subtitle: "\(d.host):\(String(d.port))",
isOnline: true,
osChain: d.osChain,
activate: { connectDiscovered(d) })
}
let add = HomeTile(
@@ -420,6 +425,15 @@ private struct GamepadHostTile: View {
Image(systemName: icon)
.font(.system(size: Self.iconFont, weight: .semibold))
.foregroundStyle(Color.brand)
} else if let mark = osIconImage(for: tile.osChain) {
// The OS mark stands in for the initial (template asset tints like the text it
// replaces), and carries the label, since nothing else on the tile names the OS.
mark
.resizable()
.scaledToFit()
.frame(width: Self.monogramFont, height: Self.monogramFont)
.foregroundStyle(tile.filled ? .white : Color.brand)
.accessibilityLabel(tile.osChain ?? "")
} else {
Text(monogram(tile.title))
.font(.geistFixed(Self.monogramFont, .bold))
@@ -38,9 +38,16 @@ private func monogram(_ name: String) -> String {
return String(first).uppercased()
}
/// The squared monogram tile. `filled` = a solid brand-purple chip (saved hosts); otherwise a
/// tinted outline (discovered hosts). Shows a spinner in place of the glyph while connecting.
private func monogramTile(_ letter: String, m: CardMetrics, connecting: Bool, filled: Bool) -> some View {
/// The squared host tile. `filled` = a solid brand-purple chip (saved hosts); otherwise a tinted
/// outline (discovered hosts). Shows a spinner in place of the glyph while connecting.
///
/// `mark` is the host's OS mark, and it REPLACES the monogram when we have one: it identifies the
/// machine better than its initial ever did, and on a row of similarly-named boxes the initial says
/// nothing the name beneath it doesn't already say. A host that advertises no OS chain or one we
/// ship no art for keeps its letter, so a mixed row still reads as one set.
private func monogramTile(
_ letter: String, osChain: String?, m: CardMetrics, connecting: Bool, filled: Bool
) -> some View {
let shape = RoundedRectangle(cornerRadius: m.radius - 3, style: .continuous)
return ZStack {
shape.fill(filled
@@ -50,6 +57,16 @@ private func monogramTile(_ letter: String, m: CardMetrics, connecting: Bool, fi
: AnyShapeStyle(Color.brand.opacity(0.14)))
if connecting {
ProgressView().tint(filled ? .white : Color.brand)
} else if let mark = osIconImage(for: osChain) {
// Template asset tints from foregroundStyle exactly like the letter it stands in for.
// Labelled, because this is where the OS is now announced: it used to ride the status
// row below, which no longer carries it.
mark
.resizable()
.scaledToFit()
.frame(width: m.monogram, height: m.monogram)
.foregroundStyle(filled ? Color.white : Color.brand)
.accessibilityLabel(osChain ?? "")
} else {
// Fixed size (not Dynamic Type): the glyph is pinned inside a fixed tile, so it must
// not scale up and spill out at large accessibility text sizes. minimumScaleFactor +
@@ -133,7 +150,8 @@ struct HostCardView: View {
let m = CardMetrics.current
return Button(action: onConnect) {
HStack(spacing: m.spacing) {
monogramTile(monogram(host.displayName), m: m, connecting: isConnecting, filled: true)
monogramTile(monogram(host.displayName), osChain: host.osChain,
m: m, connecting: isConnecting, filled: true)
VStack(alignment: .leading, spacing: 4) {
// The chip rides the TITLE line, anchored to the card's trailing edge not
// trailing the name, where it read as part of the title, and not on a line of
@@ -295,16 +313,7 @@ struct HostCardView: View {
/// certificate is pinned (the lock state, spelled out).
@ViewBuilder private func statusRow(_ m: CardMetrics) -> some View {
HStack(spacing: 6) {
// The host's OS mark leads the row (template asset tints like an SF Symbol);
// absent entirely for a host that never advertised one, so those cards render
// exactly as they always did.
if let mark = osIconImage(for: host.osChain) {
mark
.resizable()
.scaledToFit()
.frame(width: m.status + 2, height: m.status + 2)
.accessibilityLabel(host.osChain ?? "")
}
// The OS mark used to lead this row; it is the tile's glyph now (see monogramTile).
RoundedRectangle(cornerRadius: 1.5)
.fill(isOnline ? Color.green : Color.secondary.opacity(0.4))
.frame(width: 6, height: 6)
@@ -364,7 +373,8 @@ struct DiscoveredCardView: View {
let m = CardMetrics.current
return Button(action: onConnect) {
HStack(spacing: m.spacing) {
monogramTile(monogram(discovered.name), m: m, connecting: false, filled: false)
monogramTile(monogram(discovered.name), osChain: discovered.osChain,
m: m, connecting: false, filled: false)
VStack(alignment: .leading, spacing: 4) {
Text(discovered.name)
.font(.geist(m.name, .bold, relativeTo: .title3))
@@ -375,14 +385,7 @@ struct DiscoveredCardView: View {
.foregroundStyle(.secondary)
.lineLimit(1)
HStack(spacing: 6) {
// Same leading OS mark as a saved card's status row live from the advert.
if let mark = osIconImage(for: discovered.osChain) {
mark
.resizable()
.scaledToFit()
.frame(width: m.status + 2, height: m.status + 2)
.accessibilityLabel(discovered.osChain)
}
// The advert's OS mark is the tile's glyph now (see monogramTile).
Image(systemName: discovered.requiresPairing
? "lock.fill" : "antenna.radiowaves.left.and.right")
.font(.system(size: m.status))