crossmate

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

commit a3c998b35f0b2532f99c31f6559be20f63794190
parent 45f5126dc555267c19ca165f8289b88fbfde905e
Author: Michael Camilleri <[email protected]>
Date:   Fri, 19 Jun 2026 17:37:19 +0900

Use glass effect button for nudges

Diffstat:
MCrossmate/Views/Puzzle/PuzzleScoreboard.swift | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
MScripts/run-demo.sh | 67++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
2 files changed, 116 insertions(+), 25 deletions(-)

diff --git a/Crossmate/Views/Puzzle/PuzzleScoreboard.swift b/Crossmate/Views/Puzzle/PuzzleScoreboard.swift @@ -193,7 +193,7 @@ struct PuzzleScoreboard: View { private var verticalBody: some View { VStack(alignment: .leading, spacing: 12) { - playersHeading + verticalHeading VStack(alignment: .leading, spacing: 6) { ForEach(scores) { score in @@ -239,9 +239,60 @@ struct PuzzleScoreboard: View { .frame(height: horizontalHeaderHeight) } - /// The "Players" heading, shared by both layouts. When there's someone to - /// nudge it *is* the nudge button — a tinted capsule carrying the wave - /// symbol and the title that, on tap, swaps to a brief "Nudge Sent" + /// The vertical (side-panel) heading. With more horizontal room to spare, + /// "Players" stays a plain leading heading and the nudge action lives in a + /// separate accent-coloured capsule button, vertically centred on the + /// trailing side. The button is omitted entirely when there's no one to nudge. + @ViewBuilder + private var verticalHeading: some View { + HStack(spacing: 8) { + Text("Players") + .font(.headline) + if showsNudgeButton { + Spacer(minLength: 8) + nudgeButton + } + } + } + + /// The trailing nudge capsule used by `verticalHeading`. Wrapped in a ZStack + /// with the "Nudge Sent" confirmation and cross-faded with opacity, so the + /// heading reserves the capsule's footprint and nothing shifts on tap. + private var nudgeButton: some View { + ZStack(alignment: .trailing) { + Button { + sendNudge() + } label: { + nudgeCapsule + } + .buttonStyle(.plain) + .disabled(!canNudge() || showNudgeSent) + .accessibilityLabel("Nudge Players") + .opacity(showNudgeSent ? 0 : 1) + .accessibilityHidden(showNudgeSent) + + Text("Nudge Sent") + .font(.footnote.weight(.semibold)) + .foregroundStyle(.secondary) + .opacity(showNudgeSent ? 1 : 0) + .accessibilityHidden(!showNudgeSent) + } + } + + private var nudgeCapsule: some View { + HStack(spacing: 5) { + Image(systemName: "hand.wave") + Text("Nudge") + } + .font(.footnote.weight(.semibold)) + .padding(.horizontal, 12) + .padding(.vertical, 4) + .nudgeGlass(isLabeled: true) + } + + /// The horizontal (paged-header) heading. With little vertical room, the + /// "Players" heading itself *is* the nudge button — a tinted capsule carrying + /// the wave symbol and the title that, on tap, swaps to a brief "Nudge Sent" /// confirmation. Otherwise it falls back to a plain heading. @ViewBuilder private var playersHeading: some View { @@ -269,11 +320,11 @@ struct PuzzleScoreboard: View { } } else { // No one to nudge (a solo game, or no peers have joined yet): the - // plain, non-button heading, at each layout's original prominence. - // Match the capsule's vertical padding so the heading keeps the same - // height as the button form and the chips/rows below don't shift. + // plain, non-button heading. Match the capsule's vertical padding so + // the heading keeps the same height as the button form and the chips + // below don't shift. Text("Players") - .font(layout == .vertical ? .headline : .subheadline.weight(.semibold)) + .font(.subheadline.weight(.semibold)) .padding(.top, 5) } } @@ -297,10 +348,13 @@ struct PuzzleScoreboard: View { Text("Players") } .font(.footnote.weight(.semibold)) - .foregroundStyle(.white) .padding(.horizontal, 12) .padding(.vertical, 4) - .background(preferences.color.tint, in: Capsule()) + .nudgeGlass(isLabeled: true) + // The horizontal header sits on a flat white surface where clear glass + // nearly vanishes, so a faint shadow lifts the capsule enough to read + // as a button without tinting away the glass look. + .shadow(color: .black.opacity(0.18), radius: 3, y: 1) } private func scoreChip(_ score: Score) -> some View { diff --git a/Scripts/run-demo.sh b/Scripts/run-demo.sh @@ -5,18 +5,39 @@ # List colour strips, the friends list, and the puzzle scoreboard without iCloud # or a real opponent. The real on-disk store is untouched. # -# Runs on a dedicated "Crossmate Demo" simulator — created on first run, reused -# thereafter — so it never collides with whatever device Xcode has open. +# Runs on a dedicated demo simulator — created on first run, reused thereafter — +# so it never collides with whatever device Xcode has open. iPhone and iPad each +# get their own demo device, so the two can coexist. # -# Usage: bash Scripts/run-demo.sh [iOS-major] (default major: 26) -# bash Scripts/run-demo.sh --delete (remove the demo simulator) +# Usage: bash Scripts/run-demo.sh [iOS-major] (iPhone, default major 26) +# bash Scripts/run-demo.sh --ipad [iOS-major] (iPad mini) +# bash Scripts/run-demo.sh [--ipad] --delete (remove the demo simulator) set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" source "${SCRIPT_DIR}/select-simulator.sh" -DEMO_NAME="Crossmate Demo" +# Parse arguments in any order: --ipad picks the iPad demo device, --delete tears +# the resolved device down, and a bare number overrides the iOS major version. +DEVICE_KIND="iphone" +DELETE=false +MAJOR=26 +for arg in "$@"; do + case "$arg" in + --ipad) DEVICE_KIND="ipad" ;; + --iphone) DEVICE_KIND="iphone" ;; + --delete) DELETE=true ;; + [0-9]*) MAJOR="$arg" ;; + *) echo "Unknown argument: $arg" >&2; exit 1 ;; + esac +done + +if [ "$DEVICE_KIND" = "ipad" ]; then + DEMO_NAME="Crossmate Demo iPad" +else + DEMO_NAME="Crossmate Demo" +fi BUNDLE_ID="net.inqk.crossmate" DERIVED_DATA="/tmp/crossmate-demo-derived" @@ -28,7 +49,7 @@ UDID=$(xcrun simctl list devices \ | sed -E 's/.*\(([0-9A-Fa-f-]{36})\).*/\1/' || true) # `--delete` tears the demo simulator down and exits. -if [ "${1:-}" = "--delete" ]; then +if [ "$DELETE" = true ]; then if [ -n "$UDID" ]; then xcrun simctl shutdown "$UDID" 2>/dev/null || true xcrun simctl delete "$UDID" @@ -39,19 +60,35 @@ if [ "${1:-}" = "--delete" ]; then exit 0 fi -MAJOR="${1:-26}" - if [ -n "$UDID" ]; then echo "Reusing '${DEMO_NAME}' (${UDID})" else - # Create it on the newest available iOS <major> runtime, using the iPhone - # model select-simulator picks. Resolve both as CoreSimulator identifiers, - # which is what `simctl create` expects. + # Create it on the newest available iOS <major> runtime. select_simulator picks + # the runtime (and an iPhone model); for iPad we swap in the newest iPad mini. + # Resolve the device type and runtime as CoreSimulator identifiers, which is + # what `simctl create` expects. select_simulator "$MAJOR" - DEVICE_TYPE_ID=$(xcrun simctl list devicetypes \ - | grep -E "^${DEVICE} \(" \ - | head -1 \ - | sed -E 's/.*\((com\.apple[^)]*)\).*/\1/' || true) + if [ "$DEVICE_KIND" = "ipad" ]; then + # Pick the newest iPad mini without trusting the list's order: tag each one + # with the first number in its name — the chip ("A17") or generation ("6th"), + # whichever the model uses — then sort numerically and take the largest. Chip + # numbers dwarf generation numbers, so chip-named models always win. + DEVICE_TYPE_LINE=$(xcrun simctl list devicetypes \ + | grep -F "iPad mini" \ + | sed -E 's/^iPad mini[^0-9]*([0-9]+).*/\1 &/' \ + | sort -rn -k1,1 \ + | head -1 \ + | cut -f2- || true) + else + DEVICE_TYPE_LINE=$(xcrun simctl list devicetypes \ + | grep -E "^${DEVICE} \(" \ + | head -1 || true) + fi + # The display name is the line minus its trailing "(com.apple…)" identifier; + # the identifier is that parenthesised tail. Parse both from the one line so + # iPad names that themselves contain parens — "iPad mini (A17 Pro)" — survive. + DEVICE=$(sed -E 's/ \(com\.apple[^)]*\)$//' <<<"$DEVICE_TYPE_LINE") + DEVICE_TYPE_ID=$(sed -E 's/.*\((com\.apple[^)]*)\)$/\1/' <<<"$DEVICE_TYPE_LINE") RUNTIME_ID=$(xcrun simctl list runtimes available \ | grep "iOS ${RUNTIME} (" \ | head -1 \