commit 85db483057391a6da21279528457054d53875cae
parent 88645c4d32f12ab067730b7a0366ac9deea7fdb2
Author: Michael Camilleri <[email protected]>
Date: Sun, 28 Jun 2026 16:51:21 +0900
Polish the Success Panel share card
This commit refines the completion share flow so the shared card reads
more like finished presentation copy. The rendered card and accompanying
caption now wrap the puzzle title in proper single quotation marks, and
the author line uses lowercase 'by' to match the surrounding phrasing.
The card's contributor wash now pre-composites each player's colour over
white at the author-attribution opacity before drawing the grid. That
keeps the exported squares aligned with the Clue Bar's lighter wash
instead of letting translucent fills blend against the Canvas gridline
colour.
The Success Panel footer also places the share control beside the finish
summary, with the finish time and revealed-square count centred as their
own text group.
Co-Authored-By: Codex GPT 5.5 <[email protected]>
Diffstat:
1 file changed, 39 insertions(+), 13 deletions(-)
diff --git a/Crossmate/Views/Puzzle/SuccessPanel.swift b/Crossmate/Views/Puzzle/SuccessPanel.swift
@@ -77,7 +77,7 @@ struct SuccessPanel: View {
/// dropping the time clause when none is recorded or the user opted it out,
/// and the friends clause when solo.
private func shareMessage(includeClock: Bool) -> String {
- var parts = ["I solved the puzzle '\(session.puzzle.title)'"]
+ var parts = ["I solved the puzzle ‘\(session.puzzle.title)’"]
let seconds = roster.solveTime()
if includeClock, seconds > 0 {
parts.append("in \(TimeLog.clockString(seconds))")
@@ -88,18 +88,42 @@ struct SuccessPanel: View {
return parts.joined(separator: " ") + " using Crossmate"
}
- /// The tint for a filled square, using the active-word highlight fill. Keyed
- /// on authorship, not the reveal mark: a revealed square clears its author
- /// (Game.revealCells), and that nil author survives persistence even when
- /// the `.revealed` mark doesn't — so an unauthored square is reliably left
- /// untinted on both live and reloaded games. In a non-shared puzzle an
+ /// The tint for a filled square, using the lighter author-attribution wash
+ /// pre-composited over white. The share card's Canvas paints gridlines
+ /// first, so using a translucent fill would blend the wash with gray rather
+ /// than the white cell colour the Clue Bar uses.
+ /// Keyed on authorship, not the reveal mark: a revealed square clears its
+ /// author (Game.revealCells), and that nil author survives persistence even
+ /// when the `.revealed` mark doesn't — so an unauthored square is reliably
+ /// left untinted on both live and reloaded games. In a non-shared puzzle an
/// authored square is the user's own colour; in a shared puzzle it's the
/// contributing author's colour.
private func contributorTint(for authorID: String?) -> Color? {
guard let authorID else { return nil }
let hasRemotePlayers = roster.entries.contains { !$0.isLocal }
- guard hasRemotePlayers else { return preferences.color.highlightFill }
- return roster.entries.first(where: { $0.authorID == authorID })?.color.highlightFill
+ guard hasRemotePlayers else { return shareCardAuthorWash(for: preferences.color) }
+ guard let color = roster.entries.first(where: { $0.authorID == authorID })?.color else {
+ return nil
+ }
+ return shareCardAuthorWash(for: color)
+ }
+
+ private func shareCardAuthorWash(for color: PlayerColor) -> Color {
+ let resolved = UIColor(color.tint).resolvedColor(with: UITraitCollection(userInterfaceStyle: .light))
+ var red: CGFloat = 0
+ var green: CGFloat = 0
+ var blue: CGFloat = 0
+ var alpha: CGFloat = 0
+ guard resolved.getRed(&red, green: &green, blue: &blue, alpha: &alpha) else {
+ return color.authorTintFill
+ }
+
+ let washAlpha = CGFloat(PlayerColor.authorTintOpacity) * alpha
+ return Color(
+ red: Double(1 - washAlpha + red * washAlpha),
+ green: Double(1 - washAlpha + green * washAlpha),
+ blue: Double(1 - washAlpha + blue * washAlpha)
+ )
}
/// Per-cell tints for the share card's silhouette: `nil` for blocks, for
@@ -347,8 +371,8 @@ struct SuccessPanel: View {
}
}
- VStack(spacing: 8) {
- VStack(spacing: 4) {
+ HStack(alignment: .center, spacing: 8) {
+ VStack(alignment: .center, spacing: 4) {
if let finishedInText {
Text(finishedInText)
}
@@ -356,10 +380,12 @@ struct SuccessPanel: View {
}
.font(.footnote)
.foregroundStyle(.secondary)
+ .multilineTextAlignment(.center)
+
shareButton
}
.padding(.top, 8)
- .frame(maxWidth: .infinity, alignment: .center)
+ .frame(maxWidth: .infinity)
}
}
.scrollIndicators(.hidden)
@@ -651,7 +677,7 @@ private struct SuccessShareCard: View {
.padding(.vertical, 12)
VStack(spacing: 8) {
- Text("Completed \(puzzle.title)")
+ Text("Completed ‘\(puzzle.title)’")
.font(.system(size: 24, weight: .semibold, design: .rounded))
.foregroundStyle(.black)
.multilineTextAlignment(.center)
@@ -660,7 +686,7 @@ private struct SuccessShareCard: View {
if puzzle.author != nil || timeText != nil {
HStack(spacing: 8) {
if let author = puzzle.author {
- Text("By \(author)")
+ Text("by \(author)")
.lineLimit(1)
}
if let timeText {