design: drop disc drop-shadow, scale up processing-state spinner

Two design cleanups, both for visual polish on the record disc
(the hero control of the keyboard preview AND the live keyboard
extension). The shadows were a leftover from the iOS 18 mockup
that visually fought the brand-green disc; the processing-state
spinner was the system default at 1.1x, which on a 96-120pt disc
reads as "a dot in the middle" rather than "an active process".

Changes:

1) Remove the disc body's drop shadow on BOTH surfaces.
   - `KeyboardPreviewStub.swift:244` — `.shadow(color: .black.opacity(0.4),
     radius: 10, y: 6)` deleted. Hardcoded dark shadow on a
     light surface, completely theme-blind (looked like a 2009
     skeuomorphic button).
   - `RecordButton.swift:112` — `.shadow(color: .black.opacity(0.45),
     radius: 14, y: 8)` deleted. Same problem on the real
     keyboard's disc, plus the shadow competed with the existing
     "halo" radial gradient and the outer breathing ring.
   The internal "ambient glow" (radial green gradient inside the
   disc on idle) is NOT touched — that's brand colour, not a
   drop shadow, and matches the live keyboard's idle accent.

2) Scale up the processing-state spinner on BOTH surfaces.
   - Preview: `ProgressView().tint(.white).scaleEffect(1.1)` →
     `ProgressView().progressViewStyle(.circular).tint(palette.textPrimary)
     .scaleEffect(2.5)`.
     - 1.1x (~22pt) → 2.5x (~50pt) = 52% of the 96pt disc.
     - Tint changed from hardcoded `.white` to `palette.textPrimary`
       because the processing-state disc gradient is
       `surfaceElevated → surface` (light grey in light mode);
       a white spinner on a light grey disc is invisible regardless
       of scale.
   - Real keyboard: `ProgressView().scaleEffect(1.2)` →
     `ProgressView().scaleEffect(2.5)`. Same absolute spinner
     size (~50pt) — 42% of the 120pt disc — so the user gets the
     same visual weight whether they're looking at the in-app
     preview or the live keyboard. Tint was already
     `palette.textPrimary` so no change there.

Proportion rationale: ~50% of the disc diameter is the sweet
spot. 100% would crowd the disc's edge, <30% reads as
"decoration" rather than "process". 50pt absolute size matches
the same spinner across both surfaces — same weight, different
disc sizes — so the user doesn't have to re-learn the visual
language when they switch from preview to real keyboard.

Tests: 27/27 pass (no behavioural change, just visual).
Build: BUILD SUCCEEDED.

🤖 Generated with Claude Code
This commit is contained in:
Rocky
2026-06-18 21:22:33 +08:00
parent 56d0da0a51
commit 12608fe618
2 changed files with 21 additions and 4 deletions
+13 -2
View File
@@ -241,7 +241,6 @@ struct KeyboardPreviewStub: View {
.fill(discGradient)
.frame(width: 96, height: 96)
.overlay(Circle().stroke(palette.accentGlow, lineWidth: 1.5))
.shadow(color: .black.opacity(0.4), radius: 10, y: 6)
Group {
switch phase {
case .idle:
@@ -258,7 +257,19 @@ struct KeyboardPreviewStub: View {
}
.frame(width: 60, height: 32)
case .processing:
ProgressView().tint(.white).scaleEffect(1.1)
// Scaled to ~50pt inside a 96pt disc (~52%) so the
// spinner is the dominant visual element of the
// loading state without crowding the disc's edge.
// `palette.textPrimary` (instead of hardcoded
// `.white`) keeps the spinner visible in both light
// and dark themes the processing-state disc
// gradient is `surfaceElevated surface`, which is
// light in light mode and would swallow a white
// spinner.
ProgressView()
.progressViewStyle(.circular)
.tint(palette.textPrimary)
.scaleEffect(2.5)
}
}
}