crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

commit 8a88a8d61c94b3539dfd10b2b52dc11dba2e580b
parent 6b3879dec3ed689cba4b6126b2ab88cb7652d662
Author: Michael Camilleri <[email protected]>
Date:   Thu, 18 Jun 2026 10:50:41 +0900

Add a sheen and separators to the participant strip

This commit refines the participant colour strip on shared in-progress
Game List rows. Each player's segment now eases from a darker top to a
lighter bottom for a slight sheen, and a hairline separator sits between
adjacent segments so multi-player strips are easier to read apart. A
small gap now sits between the strip and the puzzle thumbnail.

The gradient varies luminance rather than opacity so each player's hue
stays intact, and it relies on 'Color.mix', which needs iOS 18; earlier
systems fall back to the flat selection fill. The separator tracks the
display scale so it stays a true hairline.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

Diffstat:
MCrossmate/Models/PlayerColor.swift | 18++++++++++++++++++
MCrossmate/Views/GameList/GameCardView.swift | 24++++++++++++++----------
2 files changed, 32 insertions(+), 10 deletions(-)

diff --git a/Crossmate/Models/PlayerColor.swift b/Crossmate/Models/PlayerColor.swift @@ -27,6 +27,24 @@ struct PlayerColor: Sendable, Identifiable, Hashable { /// Fill for UI tied to this player's active selection (the selected cell). var selectionFill: Color { tint.opacity(selectedOpacity) } + /// Vertical gradient for the Game List participant strip: the player's + /// colour eased from a darker top to a lighter bottom for a slight sheen. + /// Varying luminance (rather than opacity) keeps the hue intact while + /// staying visible across the strip's 6pt height. `Color.mix` needs + /// iOS 18; older systems fall back to the flat selection fill. + var selectionStripGradient: LinearGradient { + let stops: [Color] + if #available(iOS 18.0, *) { + stops = [ + tint.mix(with: .black, by: 0.16).opacity(selectedOpacity), + tint.mix(with: .white, by: 0.22).opacity(selectedOpacity) + ] + } else { + stops = [selectionFill, selectionFill] + } + return LinearGradient(colors: stops, startPoint: .top, endPoint: .bottom) + } + /// Fill for UI tied to this player's passive highlight — the rest of the /// active word. var highlightFill: Color { tint.opacity(highlightedOpacity) } diff --git a/Crossmate/Views/GameList/GameCardView.swift b/Crossmate/Views/GameList/GameCardView.swift @@ -118,6 +118,7 @@ struct GameListThumbnailView: View { menuParticipants: game.allParticipants ) .frame(width: 60) + .padding(.top, 2) } } .frame(width: 60) @@ -209,24 +210,27 @@ struct SharedParticipantsButton: View { } private struct ParticipantColorStrip: View { + @Environment(\.displayScale) private var displayScale let participants: [GameParticipantSummary] var body: some View { - ZStack(alignment: .topLeading) { - HStack(spacing: 0) { - if participants.isEmpty { - Rectangle() - .fill(Color.secondary.opacity(0.35)) - } else { - ForEach(participants) { participant in + HStack(spacing: 0) { + if participants.isEmpty { + Rectangle() + .fill(Color.secondary.opacity(0.35)) + } else { + ForEach(Array(participants.enumerated()), id: \.element.id) { index, participant in + if index > 0 { Rectangle() - .fill(participant.color.selectionFill) + .fill(Color(.separator)) + .frame(width: 1 / displayScale) } + Rectangle() + .fill(participant.color.selectionStripGradient) } } - .frame(height: 6) } - .frame(maxWidth: .infinity, alignment: .topLeading) + .frame(maxWidth: .infinity) .frame(height: 6) .contentShape(Rectangle()) }