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:
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 \